{{{
else:
return 'http'
}}}
And set:
`proxy_set_header X-Forwarded-Proto $scheme;`
`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.
--
Ticket URL: <https://code.djangoproject.com/ticket/27961>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.
* component: contrib.redirects => HTTP handling
Comment:
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 [https://docs.djangoproject.com/en/stable/ref/settings
/#secure-proxy-ssl-header SECURE_PROXY_SSL_HEADER] setting? By the way, if
there is a security issue here, please
[https://docs.djangoproject.com/en/dev/internals/security/#reporting-
security-issues report the issue to the security team].
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:1>
Old description:
New description:
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`
Result: ''No redirect.'' I'm not getting a ''ERR_TOO_MANY_REDIRECTS''
complain from Chrome.
2. 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'`
''No redirect.''
3. Use default setting in nginx; use a wrong setting in Django:
`proxy_set_header X-Forwarded-Proto $scheme;`
`HTTP_X_FORWARDED_PROTASDF='httpssssssss'`
''No redirect.''
4. Use custom HTTPS indicator in both nginx and Django:
proxy_set_header X-Forwarded-Protooo $scheme;
`HTTP_X_FORWARDED_PROTOOO='https'`
''No redirect.'' This is the expected behavior.
5. 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'`
Chrome complains ''ERR_TOO_MANY_REDIRECTS''. This is the expected
behavior.
6. 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'`
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.
--
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:2>
Comment (by cryptogun):
Replying to [comment:1 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 [https://docs.djangoproject.com/en/stable/ref/settings
/#secure-proxy-ssl-header SECURE_PROXY_SSL_HEADER] setting? By the way, if
there is a security issue here, please
[https://docs.djangoproject.com/en/dev/internals/security/#reporting-
security-issues 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;`
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:3>
Comment (by Tim Graham):
So case 1 is the bug case and in your settings you have
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', None)`? I can't see
from the documentation what a `None` value there is supposed to do.
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:4>
Old description:
> 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`
> Result: ''No redirect.'' I'm not getting a ''ERR_TOO_MANY_REDIRECTS''
> complain from Chrome.
>
> 2. 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'`
> ''No redirect.''
>
> 3. Use default setting in nginx; use a wrong setting in Django:
> `proxy_set_header X-Forwarded-Proto $scheme;`
> `HTTP_X_FORWARDED_PROTASDF='httpssssssss'`
> ''No redirect.''
>
> 4. Use custom HTTPS indicator in both nginx and Django:
> proxy_set_header X-Forwarded-Protooo $scheme;
> `HTTP_X_FORWARDED_PROTOOO='https'`
> ''No redirect.'' This is the expected behavior.
>
> 5. 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'`
> Chrome complains ''ERR_TOO_MANY_REDIRECTS''. This is the expected
> behavior.
>
> 6. 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'`
> 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.
New description:
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.
2. 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.''
3. 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.''
4. 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.
5. 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.
6. 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.
--
Comment (by cryptogun):
Replying to [comment:4 Tim Graham]:
> So case 1 is the bug case and in your settings you have
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', None)`? I can't see
from the documentation what a `None` value there is supposed to do.
Sorry, I forgot that it is SECURE_PROXY_SSL_HEADER setting. Re-updated my
main ticket.
There're 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.
- I 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`).
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:5>
Old description:
> I'm using nginx + gunicorn and display pages via HTTPS:
> 1. Both default settings:
> Nginx setting: `proxy_set_header X-Forwarded-Proto $scheme;`
> 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.
New description:
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: `SECURE_PROXY_SSL_HEADER = None`
Result: ''No redirect.'' I'm not getting a ''ERR_TOO_MANY_REDIRECTS''
complain from Chrome.
2. Use default setting in nginx; use a wrong setting in Django, i.e. the
'httpsssss' part:
`proxy_set_header X-Forwarded-Proto $scheme;`
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'httpssssssss')`
''No redirect.''
3. Use default setting in nginx; use a wrong setting in Django:
`proxy_set_header X-Forwarded-Proto $scheme;`
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTASDF', 'httpssssssss')`
''No redirect.''
4. Use custom HTTPS indicator in both nginx and Django:
proxy_set_header X-Forwarded-Protooo $scheme;
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOOO', 'https')`
''No redirect.'' This is the expected behavior.
5. Use custom HTTPS indicator in both nginx and Django, and testing for a
unsafe protocol ( != 'https'):
proxy_set_header X-Forwarded-Protooo $scheme;
`SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTOOO', 'httpsssss')`
Chrome complains ''ERR_TOO_MANY_REDIRECTS''. This is the expected
behavior.
6. A fix testing by myself:
Add an else clause under
[https://github.com/django/django/blob/001cf53280f7e2b72199237f37c340d2639fe143/django/http/request.py#L196-L197
these lines].
{{{
else:
return 'http'
}}}
And set:
`proxy_set_header X-Forwarded-Proto $scheme;`
`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.
--
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:6>
Comment (by cryptogun):
Update. There is no security vulnerability here. The problem is: A
misconfigured setting `SECURE_PROXY_SSL_HEADER` is ignored.
Explanation:
{{{
@property
def scheme(self):
if settings.SECURE_PROXY_SSL_HEADER:
try:
header, value = settings.SECURE_PROXY_SSL_HEADER
except ValueError:
raise ImproperlyConfigured(
'The SECURE_PROXY_SSL_HEADER setting must be a tuple
containing two values.'
)
if self.META.get(header) == value:
return 'https'
return self._get_scheme()
}}}
If `self.META.get(header) != value`, outer `if` ended with no `return`, so
the last line `return self._get_scheme()` is executed. i.e.
- A misconfigured setting `SECURE_PROXY_SSL_HEADER =
('HTTP_X_FORWARDED_PROTO', 'httpssssssss')` is ignored without giving a
warning and then fallback to `self._get_scheme()`.
- `_get_scheme()` is overridden to `return
self.environ.get('wsgi.url_scheme')`. See
[https://github.com/django/django/blob/master/django/core/handlers/wsgi.py#L105
here].
- Gunicorn sets that environ. See
[https://github.com/benoitc/gunicorn/blob/master/gunicorn/http/wsgi.py#L140-L142
here].
- Gunicorn sets that environ by comparing request header to gunicorn
setting `secure_scheme_headers` (default is `{'X-FORWARDED-PROTOCOL':
'ssl', 'X-FORWARDED-PROTO': 'https', 'X-FORWARDED-SSL': 'on'}`). See
[http://docs.gunicorn.org/en/stable/settings.html#secure-scheme-headers
here].
As for my 5 case:
Case 2 and 3 are misconfigurating `SECURE_PROXY_SSL_HEADER` in django.
Scheme is determined by gunicorn without a warning.
Case 1, 4, 5: no problem, safe, works great.
2.
nginx sets header: `proto` (Abbr. of `X-Forwarded-Proto` header, you know
what I mean.)
django gets scheme by checking this header: `proto == httpssssssss` (Abbr.
of `SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'httpssssssss')`)
gunicorn gets scheme by checking these header: `proto ssl protocol`
result: HTTP: redirect. Then Chrome re-request via HTTPS. HTTPS: accept.
conclusion: Safe. Fallback to gunicorn.
3.
nginx: `proto`
django: `protasdf == httpssssssss`
gunicorn: `proto ssl protocol`
result: HTTP: redirect. HTTPS: accept.
conclusion: Safe. Fallback to gunicorn.
1.
nginx: `proto`
django: `None`
gunicorn: `proto ssl protocol`
result: HTTP: redirect. HTTPS: accept.
conclusion: Safe. Django let gunicorn to determine the scheme.
4.
nginx: `protooo`
gunicorn: `proto ssl protocol`
django: `protooo == https`
result: HTTP: redirect. HTTPS: accept.
conclusion: Safe. Django determines the scheme.
5.
nginx: `protooo`
gunicorn: `proto ssl protocol`
django: `protooo == httpsssss`
result: HTTP: redirect. HTTPS: redirect.
conclusion: Safe, redirect anything. Django determines the scheme.
`httpsssss != https`, so HTTPS also redirected.
6.
nginx: `proto`
gunicorn: `proto ssl protocol`
django: `proto == httpssssssss`
result: HTTP: redirect. HTTPS: redirect.
conclusion: Safe. An else clause was added so the code won't fallback to
gunicorn. Django determines the scheme, in contrast with case 2.
`httpssssssss != https`, so HTTPS also redirected.
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:7>
* stage: Unreviewed => Accepted
Comment:
I have investigated in detail but the proposed change seems reasonable at
first glance.
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:8>
Comment (by Jacob Walls):
Looks like the proposed change was made in
54d0f5e62f54c29a12dd96f44bacd810cbe03ac8.
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:9>
* status: new => closed
* resolution: => fixed
--
Ticket URL: <https://code.djangoproject.com/ticket/27961#comment:10>