I'm revising my stance on this. After further digging around, I'm gonna go with Niphlod's position that securing only the login traffic without securing the entire session is for the most part pretty worthless. While this might have value to some sites that have to deal with mixed content, the complexity it introduces isn't worth it.
I'm also taking back my recommendation that we need to have a setting to explicitly allow SSL traffic. I think it's fine to just check the headers for forwarded SSL traffic and trust that it is. Yes, headers can be spoofed, but I can't think of how this could be exploited on the user end- So that leaves only *two recommended changes:* - When checking whether HTTPS, check for forwarded SSL headers with if request.env.http_x_forwarded_proto in ['https', 'HTTPS']: - Add a auth.secure = True convenience setting, which would call requires_https() while the user is logged in, and on all login/registration methods. I'll update the ticket On Friday, September 21, 2012 2:26:36 PM UTC-4, Yarin wrote: > > Done http://code.google.com/p/web2py/issues/detail?id=1023 > > On Friday, September 21, 2012 2:05:41 PM UTC-4, Massimo Di Pierro wrote: >> >> Yarin, please open an issue on google code as suggested enhancement so ti >> does not get lost. Also feel free to move the discussion on web2py >> developers. >> >> >> On Friday, 21 September 2012 12:22:57 UTC-5, Yarin wrote: >>> >>> Here's a complete example of our own implementation (simplified, >>> untested) using the proposed auth settings: >>> >>> *In our model:* >>> >>> def force_https(trust_proxy = False, secure_session = False): >>> """ Enforces HTTPS in appropriate environments >>> >>> Args: >>> trust_proxy: Can we trust proxy header 'http_x_forwarded_proto' >>> to determine SSL. >>> (Set this only if ALL your traffic comes via trusted proxy.) >>> secure_session: Secure the session as well. >>> (Do this only when enforcing SSL throughout the session) >>> """ >>> >>> # If cronjob or scheduler, exit: >>> cronjob = request.global_settings.cronjob >>> cmd_options = request.global_settings.cmd_options >>> if cronjob or (cmd_options and cmd_options.scheduler): >>> return >>> >>> # If local host, exit: >>> if request.env.remote_addr == "127.0.0.1": >>> return >>> >>> # If already HTTPS, exit: >>> if request.env.wsgi_url_scheme in ['https', 'HTTPS']: >>> if secure_session: >>> current.session.secure() >>> return >>> >>> # If HTTPS request forwarded over HTTP via a SSL-terminating proxy, >>> exit: >>> if trust_proxy and request.env.http_x_forwarded_proto in ['https', >>> 'HTTPS']: >>> if secure_session: >>> current.session.secure() >>> return >>> >>> # Redirect to HTTPS: >>> redirect(URL(scheme='https', args=request.args, vars=request.vars)) >>> >>> # If a login function, force SSL: >>> if request.controller == 'default' and request.function == 'user' andauth >>> .settings.force_ssl_login: >>> force_https(trust_proxy = auth.settings.is_proxied, secure_session >>> = auth.settings.force_ssl_session) >>> # If user is logged in and we're enforcing a full SSL session: >>> elif auth.is_logged_in() and auth.settings.force_ssl_session: >>> force_https(trust_proxy = auth.settings.is_proxied, secure_session = >>> True) >>> >>> def on_login(form): >>> """ Post login redirection""" >>> >>> # If we're enforcing SSL on login only, redirect from HTTPS to HTTP >>> immediately after login: >>> if auth.settings.force_ssl_login is True and >>> auth.settings.force_ssl_session >>> is False: >>> if request.env.wsgi_url_scheme in ['https', 'HTTPS'] or request. >>> env.http_x_forwarded_proto in ['https', 'HTTPS']: >>> >>> # Extract the post-login url value from auth >>> # (hack - look at end of login() function in tools.py. This >>> belongs in Auth itself.): >>> login_next_path = auth.next or auth.settings.login_next >>> # Build an absolute, HTTP url from it: >>> login_next_url = URL(scheme='http',c='default',f='index') >>> +login_next_path >>> [1:] >>> # Redirect to the HTTP URL: >>> redirect(login_next_url) >>> >>> auth.settings.login_onaccept = on_login >>> >>> >>> >>> >>> On Friday, September 21, 2012 12:35:37 PM UTC-4, Yarin wrote: >>>> >>>> You can't detect this- it must be a setting. Please see my previous >>>> answer: >>>> https://groups.google.com/forum/#!msg/web2py/me1e5d6Dudk/VQRQhdiryccJ >>>> >>>> "you cannot detect whether proxied traffic is real because headers are >>>> unreliable. Instead you must securely set up a server behind a proxy and >>>> set the .is_proxied flag explicitly." >>>> >>>> "you can't mix direct and proxied traffic. To be able to handle >>>> proxy-terminated SSL, we need to know that *all* the traffic is via a >>>> trusted proxy." >>>> >>>> >>>> On Friday, September 21, 2012 12:05:56 PM UTC-4, Massimo Di Pierro >>>> wrote: >>>>> >>>>> Yes but how do you detect if is_proxied reliably? >>>>> >>>>> On Friday, 21 September 2012 10:28:26 UTC-5, Yarin wrote: >>>>>> >>>>>> FYI this is the enforcer function we wrote for our implementation- >>>>>> basically a rewrite of request.requires_https(): >>>>>> >>>>>> def force_https(trust_proxy = False): >>>>>> """ Enforces HTTPS in appropriate environments >>>>>> >>>>>> Args: >>>>>> trust_proxy: Can we trust proxy header 'http_x_forwarded_proto' >>>>>> to determine SSL. >>>>>> (Set this only if ALL your traffic comes via trusted proxy.) >>>>>> """ >>>>>> >>>>>> # If cronjob or scheduler, exit: >>>>>> cronjob = request.global_settings.cronjob >>>>>> cmd_options = request.global_settings.cmd_options >>>>>> if cronjob or (cmd_options and cmd_options.scheduler): >>>>>> return >>>>>> >>>>>> # If local host, exit: >>>>>> if request.env.remote_addr == "127.0.0.1": >>>>>> return >>>>>> >>>>>> # If already HTTPS, exit: >>>>>> if request.env.wsgi_url_scheme in ['https', 'HTTPS']: >>>>>> return >>>>>> >>>>>> # If HTTPS request forwarded over HTTP via SSL-terminating proxy, >>>>>> exit: >>>>>> if trust_proxy and request.env.http_x_forwarded_proto in ['https', >>>>>> 'HTTPS']: >>>>>> return >>>>>> >>>>>> # Redirect to HTTPS: >>>>>> redirect(URL(scheme='https', args=request.args, vars=request.vars)) >>>>>> >>>>>> >>>>>> >>>>>> >>>>>> >>>>>> On Friday, September 21, 2012 9:53:36 AM UTC-4, Yarin wrote: >>>>>>> >>>>>>> The completely naive approach would be to do: >>>>>>> >>>>>>> if request.env.http_x_forwarded_for and \ >>>>>>> request.env.http_x_forwarded_proto in ['https', 'HTTPS']: >>>>>>> # Is HTTPS... >>>>>>> >>>>>>> But you cannot detect whether proxied traffic is real because >>>>>>> headers are unreliable. Instead it is up to the user to securely set up >>>>>>> a >>>>>>> server behind a proxy and set the .is_proxied flag themselves. >>>>>>> >>>>>>> *Example:* >>>>>>> We put our app server behind an SSL-terminating load balancer on the >>>>>>> cloud. The domain app.example.com points to the loadbalancer, so we >>>>>>> configure app server's Apache to allow traffic from that domain only, >>>>>>> and >>>>>>> block any outside direct traffic. Then we set * >>>>>>> auth.settings.is_proxied* to tell web2py "this proxy traffic is >>>>>>> legit" >>>>>>> >>>>>>> HTTPS/443 requests will hit the loadbalancer, and be transformed to >>>>>>> HTTP/80 traffic with *http_x_forwarded_for* and * >>>>>>> http_x_forwarded_proto* headers set. Now we can confidently check: >>>>>>> >>>>>>> if auth.settings.is_proxied and \ >>>>>>> request.env.http_x_forwarded_proto in ['https', 'HTTPS']: >>>>>>> # Is HTTPS... >>>>>>> >>>>>>> In other words *http_x_forwarded_for* header is useless and you >>>>>>> can't mix direct and proxied traffic. To be able to handle >>>>>>> proxy-terminated >>>>>>> SSL, we need to know that *all* the traffic is via a trusted proxy. >>>>>>> >>>>>>> >>>>>>> On Friday, September 21, 2012 8:40:35 AM UTC-4, Massimo Di Pierro >>>>>>> wrote: >>>>>>>> >>>>>>>> Can you suggest a way to detect that? >>>>>>>> >>>>>>>> On Thursday, 20 September 2012 13:56:55 UTC-5, Yarin wrote: >>>>>>>>> >>>>>>>>> @Massimo - that'd be great. >>>>>>>>> >>>>>>>>> One more kink to throw in is recognizing proxied SSL calls. This >>>>>>>>> requires knowing whether you can trust the traffic headers (e.g. >>>>>>>>> having >>>>>>>>> apache locked down to all traffic except your load balancer), so >>>>>>>>> maybe we >>>>>>>>> need a trust_proxied_ssl or is_proxied setting somewhere? >>>>>>>>> >>>>>>>>> if request.env.http_x_forwarded_for and >>>>>>>>> request.env.http_x_forwarded_proto >>>>>>>>> in ['https', 'HTTPS'] and auth.settings.is_proxied: >>>>>>>>> >>>>>>>>> >>>>>>>>> >>>>>>>>> On Thursday, September 20, 2012 12:52:22 PM UTC-4, Massimo Di >>>>>>>>> Pierro wrote: >>>>>>>>>> >>>>>>>>>> I think we should do something like this. >>>>>>>>>> >>>>>>>>>> I think we should have auth.settings.force_ssl_login >>>>>>>>>> and auth.settings.force_ssl_login. >>>>>>>>>> We could add secure=True option to existing requires validators. >>>>>>>>>> >>>>>>>>>> This should not be enforced from localhost. >>>>>>>>>> >>>>>>>>>> >>>>>>>>>> On Thursday, 20 September 2012 09:07:14 UTC-5, Yarin wrote: >>>>>>>>>>> >>>>>>>>>>> A proposal for improving SSL support in web2py >>>>>>>>>>> >>>>>>>>>>> For authenticated web applications, there are two "grades" of >>>>>>>>>>> SSL implementions: Forcing SSL on login, vs forcing SSL on the >>>>>>>>>>> entire >>>>>>>>>>> authenticated session. >>>>>>>>>>> >>>>>>>>>>> In the first case, HTTPS is forced on login/registration, but >>>>>>>>>>> reverts back to HTTP upon authentication. This protects against >>>>>>>>>>> passwords >>>>>>>>>>> from being sent unencrypted, but won't prevent session hijacking as >>>>>>>>>>> the >>>>>>>>>>> session cookie can still be compromised on subsequent HTTP >>>>>>>>>>> requests. (See >>>>>>>>>>> Firesheep <http://codebutler.com/firesheep> for details). >>>>>>>>>>> Nonetheless, many sites choose this approach for performance >>>>>>>>>>> reasons, as >>>>>>>>>>> SSL-delivered content is not cached by browsers as efficiently >>>>>>>>>>> (discussed >>>>>>>>>>> on 37signals >>>>>>>>>>> blog<http://37signals.com/svn/posts/1431-mixed-content-warning-how-i-loathe-thee> >>>>>>>>>>> ). >>>>>>>>>>> >>>>>>>>>>> In the second case, the entire authenticated session is secured >>>>>>>>>>> by forcing all traffic to go over HTTPS while a user is logged in >>>>>>>>>>> *and* by securing the session cookie so that it will only be >>>>>>>>>>> sent by the browser over HTTPS. >>>>>>>>>>> >>>>>>>>>>> (Also discussed in web2py users group - Auth over >>>>>>>>>>> SSL<https://groups.google.com/d/msg/web2py/7qoHMs-4Va8/jRFOqYHri4gJ> >>>>>>>>>>> ) >>>>>>>>>>> >>>>>>>>>>> web2py should make it easier to deal with these scenarios. I >>>>>>>>>>> just implemented a case-1 type solution and it took quite a bit of >>>>>>>>>>> work. >>>>>>>>>>> >>>>>>>>>>> Moreover, web2py currently provides two SSL-control functions, >>>>>>>>>>> which, taken on their own, can lead to problems for the uninitiated: >>>>>>>>>>> >>>>>>>>>>> - session.secure() will ensure that the session cookie is >>>>>>>>>>> only transmitted over HTTPS, but doesn't force HTTPS, so that >>>>>>>>>>> for any >>>>>>>>>>> subsequent session calls made over HTTP will simply not have >>>>>>>>>>> access to the >>>>>>>>>>> auth session, but this is not obvious (Correct me if I'm wrong) >>>>>>>>>>> - request.requires_https() (undocumented?) is a misnomer, >>>>>>>>>>> because if forces HTTPS but then assumes a case-2 scenario >>>>>>>>>>> and secures the session cookie >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> *Proposals:* >>>>>>>>>>> >>>>>>>>>>> - SSL auth settings >>>>>>>>>>> - auth.settings.force_ssl_login - Forces HTTPS for >>>>>>>>>>> login/registration >>>>>>>>>>> - auth.settings.force_ssl_session - Forces HTTPS >>>>>>>>>>> throughout an authenticated session, and secure the session >>>>>>>>>>> cookie (If >>>>>>>>>>> True, force_ssl_login not necessary) >>>>>>>>>>> - Other more granular controls >>>>>>>>>>> - @requires_https() - decorator for controller functions >>>>>>>>>>> that forces HTTPS for that function only >>>>>>>>>>> - 'secure=True' option on forms ensures submission over >>>>>>>>>>> HTTPS >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> >>>>>>>>>>> --