Opened 8 years ago
Closed 3 years ago
#27961 closed Bug (fixed)
Misconfigured SECURE_PROXY_SSL_HEADER setting is ignored
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 )
I'm using nginx + gunicorn and display pages via HTTPS:
- 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.
- 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.
- 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.
- 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.
- 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.
- A fix testing by myself:
Add an else clause under 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.
Change History (9)
follow-up: 3 comment:1 by , 8 years ago
Component: | contrib.redirects → HTTP handling |
---|
comment:2 by , 8 years ago
Description: | modified (diff) |
---|
comment:3 by , 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;
comment:5 by , 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).
comment:6 by , 8 years ago
Description: | modified (diff) |
---|
comment:7 by , 8 years ago
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 toself._get_scheme()
. _get_scheme()
is overridden toreturn self.environ.get('wsgi.url_scheme')
. See here.
- Gunicorn sets that environ. See 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 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.
comment:8 by , 8 years ago
Summary: | HTTP_X_FORWARDED_PROTO is bypassed → Misconfigured SECURE_PROXY_SSL_HEADER setting is ignored |
---|---|
Triage Stage: | Unreviewed → Accepted |
I have investigated in detail but the proposed change seems reasonable at first glance.
comment:9 by , 3 years ago
Looks like the proposed change was made in 54d0f5e62f54c29a12dd96f44bacd810cbe03ac8.
comment:10 by , 3 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
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.