Opened 8 years ago

Last modified 3 years ago

#27961 closed Bug

HTTP_X_FORWARDED_PROTO is bypassed — at Version 5

Reported by: cryptogun Owned by: nobody
Component: HTTP handling Version: 1.10
Severity: Normal Keywords: redirect HTTPS X-Forwarded-Proto
Cc: Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by cryptogun)

I'm using nginx + gunicorn and display pages via HTTPS:

  1. Both default settings:

Nginx setting: proxy_set_header X-Forwarded-Proto $scheme;
Django setting: HTTP_X_FORWARDED_PROTO=None SECURE_PROXY_SSL_HEADER = None
Result: No redirect. I'm not getting a ERR_TOO_MANY_REDIRECTS complain from Chrome.

  1. Use default setting in nginx; use a wrong setting in Django, i.e. the 'httpsssss' part:

proxy_set_header X-Forwarded-Proto $scheme;
HTTP_X_FORWARDED_PROTO='httpssssssss' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'httpssssssss')
No redirect.

  1. Use default setting in nginx; use a wrong setting in Django:

proxy_set_header X-Forwarded-Proto $scheme;
HTTP_X_FORWARDED_PROTASDF='httpssssssss' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTASDF', 'httpssssssss')
No redirect.

  1. Use custom HTTPS indicator in both nginx and Django:

proxy_set_header X-Forwarded-Protooo $scheme;
HTTP_X_FORWARDED_PROTOOO='https' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOOO', 'https')
No redirect. This is the expected behavior.

  1. Use custom HTTPS indicator in both nginx and Django, and testing for a unsafe protocol ( != 'https'):

proxy_set_header X-Forwarded-Protooo $scheme;
HTTP_X_FORWARDED_PROTOOO='httpsssss' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOOO', 'httpsssss')
Chrome complains ERR_TOO_MANY_REDIRECTS. This is the expected behavior.

  1. A fix testing by myself:

Add an else clause under [these lines](https://github.com/django/django/blob/master/django/http/request.py#L196-L197).

            else:
                return 'http'

And set:
proxy_set_header X-Forwarded-Proto $scheme;
HTTP_X_FORWARDED_PROTO='httpssssssss' SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'httpssssssss')
Chrome would report the expected ERR_TOO_MANY_REDIRECTS.

Did someone forget to add the else clause, or there are 3 'http' 'ftp' and 'ftps' scheme left?
If a site use 5. An attacker may set request X-Forwarded-Proto header to bypass the HTTPS check and result in 1,2,3.

Change History (4)

comment:1 by Tim Graham, 8 years ago

Component: contrib.redirectsHTTP handling

I'm having trouble understanding the report. What is the second line in each item (e.g. HTTP_X_FORWARDED_PROTO=None supposed to represent? Values of the SECURE_PROXY_SSL_HEADER setting? By the way, if there is a security issue here, please report the issue to the security team.

comment:2 by cryptogun, 8 years ago

Description: modified (diff)

in reply to:  1 comment:3 by cryptogun, 8 years ago

Replying to Tim Graham:

I'm having trouble understanding the report. What is the second line in each item (e.g. HTTP_X_FORWARDED_PROTO=None supposed to represent? Values of the SECURE_PROXY_SSL_HEADER setting? By the way, if there is a security issue here, please report the issue to the security team.

Yes your guess is right. Django sets HTTP_X_FORWARDED_PROTO to None by default as the link you provided tells. I've updated the main thread to make it more clear.
I think this is a small issue so not reported it to the security team. Since normally we do a HTTP to HTTPS redirect in nginx by this setting listen: 80; return 302 https://$server_name$request_uri;

in reply to:  4 comment:5 by cryptogun, 8 years ago

Description: modified (diff)

There are 2 bug cases:

  • In case 1, with no Django complain, user don't know whether his/her setting is correct and whether the HTTPS setup correctly.
  • In case 4, using a custom header by admin, a MITM may happen by:

user -- HTTP -- MITM(retrive password, set header: HTTP_X_FORWARDED_PROTO: https) -- nginx(header: HTTP_X_FORWARDED_PROTO: https, set header: HTTP_X_FORWARDED_PROOO: http) -- gunicorn(Pass, no user redirect because HTTP_X_FORWARDED_PROTO == https) -- Django(is_secure() == True because there's no else clause).

Last edited 8 years ago by Tim Graham (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top