Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d7597b3

Browse files
carljmtimgraham
authored andcommitted
[1.6.x] Stripped headers containing underscores to prevent spoofing in WSGI environ.
This is a security fix. Disclosure following shortly. Thanks to Jedediah Smith for the report.
1 parent f143e25 commit d7597b3

File tree

5 files changed

+142
-0
lines changed

5 files changed

+142
-0
lines changed

django/core/servers/basehttp.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ def log_message(self, format, *args):
157157

158158
sys.stderr.write(msg)
159159

160+
def get_environ(self):
161+
# Strip all headers with underscores in the name before constructing
162+
# the WSGI environ. This prevents header-spoofing based on ambiguity
163+
# between underscores and dashes both normalized to underscores in WSGI
164+
# env vars. Nginx and Apache 2.4+ both do this as well.
165+
for k, v in self.headers.items():
166+
if '_' in k:
167+
del self.headers[k]
168+
169+
return super(WSGIRequestHandler, self).get_environ()
170+
160171

161172
def run(addr, port, wsgi_handler, ipv6=False, threading=False):
162173
server_address = (addr, port)

docs/howto/auth-remote-user.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,22 @@ If your authentication mechanism uses a custom HTTP header and not
6464
class CustomHeaderMiddleware(RemoteUserMiddleware):
6565
header = 'HTTP_AUTHUSER'
6666

67+
.. warning::
68+
69+
Be very careful if using a ``RemoteUserMiddleware`` subclass with a custom
70+
HTTP header. You must be sure that your front-end web server always sets or
71+
strips that header based on the appropriate authentication checks, never
72+
permitting an end-user to submit a fake (or "spoofed") header value. Since
73+
the HTTP headers ``X-Auth-User`` and ``X-Auth_User`` (for example) both
74+
normalize to the ``HTTP_X_AUTH_USER`` key in ``request.META``, you must
75+
also check that your web server doesn't allow a spoofed header using
76+
underscores in place of dashes.
77+
78+
This warning doesn't apply to ``RemoteUserMiddleware`` in its default
79+
configuration with ``header = 'REMOTE_USER'``, since a key that doesn't
80+
start with ``HTTP_`` in ``request.META`` can only be set by your WSGI
81+
server, not directly from an HTTP request header.
82+
6783
If you need more control, you can create your own authentication backend
6884
that inherits from :class:`~django.contrib.auth.backends.RemoteUserBackend` and
6985
override one or more of its attributes and methods.

docs/releases/1.4.18.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,30 @@ Django 1.4.18 release notes
77
Django 1.4.18 fixes several security issues in 1.4.17 as well as a regression
88
on Python 2.5 in the 1.4.17 release.
99

10+
WSGI header spoofing via underscore/dash conflation
11+
===================================================
12+
13+
When HTTP headers are placed into the WSGI environ, they are normalized by
14+
converting to uppercase, converting all dashes to underscores, and prepending
15+
`HTTP_`. For instance, a header ``X-Auth-User`` would become
16+
``HTTP_X_AUTH_USER`` in the WSGI environ (and thus also in Django's
17+
``request.META`` dictionary).
18+
19+
Unfortunately, this means that the WSGI environ cannot distinguish between
20+
headers containing dashes and headers containing underscores: ``X-Auth-User``
21+
and ``X-Auth_User`` both become ``HTTP_X_AUTH_USER``. This means that if a
22+
header is used in a security-sensitive way (for instance, passing
23+
authentication information along from a front-end proxy), even if the proxy
24+
carefully strips any incoming value for ``X-Auth-User``, an attacker may be
25+
able to provide an ``X-Auth_User`` header (with underscore) and bypass this
26+
protection.
27+
28+
In order to prevent such attacks, both Nginx and Apache 2.4+ strip all headers
29+
containing underscores from incoming requests by default. Django's built-in
30+
development server now does the same. Django's development server is not
31+
recommended for production use, but matching the behavior of common production
32+
servers reduces the surface area for behavior changes during deployment.
33+
1034
Bugfixes
1135
========
1236

docs/releases/1.6.10.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,27 @@ Django 1.6.10 release notes
55
*Under development*
66

77
Django 1.6.10 fixes several security issues in 1.6.9.
8+
9+
WSGI header spoofing via underscore/dash conflation
10+
===================================================
11+
12+
When HTTP headers are placed into the WSGI environ, they are normalized by
13+
converting to uppercase, converting all dashes to underscores, and prepending
14+
`HTTP_`. For instance, a header ``X-Auth-User`` would become
15+
``HTTP_X_AUTH_USER`` in the WSGI environ (and thus also in Django's
16+
``request.META`` dictionary).
17+
18+
Unfortunately, this means that the WSGI environ cannot distinguish between
19+
headers containing dashes and headers containing underscores: ``X-Auth-User``
20+
and ``X-Auth_User`` both become ``HTTP_X_AUTH_USER``. This means that if a
21+
header is used in a security-sensitive way (for instance, passing
22+
authentication information along from a front-end proxy), even if the proxy
23+
carefully strips any incoming value for ``X-Auth-User``, an attacker may be
24+
able to provide an ``X-Auth_User`` header (with underscore) and bypass this
25+
protection.
26+
27+
In order to prevent such attacks, both Nginx and Apache 2.4+ strip all headers
28+
containing underscores from incoming requests by default. Django's built-in
29+
development server now does the same. Django's development server is not
30+
recommended for production use, but matching the behavior of common production
31+
servers reduces the surface area for behavior changes during deployment.

tests/servers/test_basehttp.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sys
2+
3+
from django.core.servers.basehttp import WSGIRequestHandler
4+
from django.test import TestCase
5+
from django.utils.six import BytesIO, StringIO
6+
7+
8+
class Stub(object):
9+
def __init__(self, **kwargs):
10+
self.__dict__.update(kwargs)
11+
12+
13+
class WSGIRequestHandlerTestCase(TestCase):
14+
15+
def test_strips_underscore_headers(self):
16+
"""WSGIRequestHandler ignores headers containing underscores.
17+
18+
This follows the lead of nginx and Apache 2.4, and is to avoid
19+
ambiguity between dashes and underscores in mapping to WSGI environ,
20+
which can have security implications.
21+
"""
22+
def test_app(environ, start_response):
23+
"""A WSGI app that just reflects its HTTP environ."""
24+
start_response('200 OK', [])
25+
http_environ_items = sorted(
26+
'%s:%s' % (k, v) for k, v in environ.items()
27+
if k.startswith('HTTP_')
28+
)
29+
yield (','.join(http_environ_items)).encode('utf-8')
30+
31+
rfile = BytesIO()
32+
rfile.write(b"GET / HTTP/1.0\r\n")
33+
rfile.write(b"Some-Header: good\r\n")
34+
rfile.write(b"Some_Header: bad\r\n")
35+
rfile.write(b"Other_Header: bad\r\n")
36+
rfile.seek(0)
37+
38+
# WSGIRequestHandler closes the output file; we need to make this a
39+
# no-op so we can still read its contents.
40+
class UnclosableBytesIO(BytesIO):
41+
def close(self):
42+
pass
43+
44+
wfile = UnclosableBytesIO()
45+
46+
def makefile(mode, *a, **kw):
47+
if mode == 'rb':
48+
return rfile
49+
elif mode == 'wb':
50+
return wfile
51+
52+
request = Stub(makefile=makefile)
53+
server = Stub(base_environ={}, get_app=lambda: test_app)
54+
55+
# We don't need to check stderr, but we don't want it in test output
56+
old_stderr = sys.stderr
57+
sys.stderr = StringIO()
58+
try:
59+
# instantiating a handler runs the request as side effect
60+
WSGIRequestHandler(request, '192.168.0.2', server)
61+
finally:
62+
sys.stderr = old_stderr
63+
64+
wfile.seek(0)
65+
body = list(wfile.readlines())[-1]
66+
67+
self.assertEqual(body, b'HTTP_SOME_HEADER:good')

0 commit comments

Comments
 (0)