summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Meyer <carl@oddbird.net>2014-09-10 11:06:19 -0600
committerTim Graham <timograham@gmail.com>2015-01-13 13:02:56 -0500
commit41b4bc73ee0da7b2e09f4af47fc1fd21144c710f (patch)
tree012e80ac5c3d5e4be839ca0bf4546f9c00f99d56
parent33f1ccf5b1a928b8680e25b3e419834d139e04e8 (diff)
[1.7.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.
-rw-r--r--django/core/servers/basehttp.py11
-rw-r--r--docs/howto/auth-remote-user.txt16
-rw-r--r--docs/releases/1.4.18.txt24
-rw-r--r--docs/releases/1.6.10.txt24
-rw-r--r--docs/releases/1.7.3.txt22
-rw-r--r--tests/servers/test_basehttp.py67
6 files changed, 164 insertions, 0 deletions
diff --git a/django/core/servers/basehttp.py b/django/core/servers/basehttp.py
index 090a694c3e..cc1ff9b4a1 100644
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -155,6 +155,17 @@ class WSGIRequestHandler(simple_server.WSGIRequestHandler, object):
sys.stderr.write(msg)
+ def get_environ(self):
+ # Strip all headers with underscores in the name before constructing
+ # the WSGI environ. This prevents header-spoofing based on ambiguity
+ # between underscores and dashes both normalized to underscores in WSGI
+ # env vars. Nginx and Apache 2.4+ both do this as well.
+ for k, v in self.headers.items():
+ if '_' in k:
+ del self.headers[k]
+
+ return super(WSGIRequestHandler, self).get_environ()
+
def run(addr, port, wsgi_handler, ipv6=False, threading=False):
server_address = (addr, port)
diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt
index 2edab6bc53..dc96a98bbc 100644
--- a/docs/howto/auth-remote-user.txt
+++ b/docs/howto/auth-remote-user.txt
@@ -64,6 +64,22 @@ If your authentication mechanism uses a custom HTTP header and not
class CustomHeaderMiddleware(RemoteUserMiddleware):
header = 'HTTP_AUTHUSER'
+.. warning::
+
+ Be very careful if using a ``RemoteUserMiddleware`` subclass with a custom
+ HTTP header. You must be sure that your front-end web server always sets or
+ strips that header based on the appropriate authentication checks, never
+ permitting an end-user to submit a fake (or "spoofed") header value. Since
+ the HTTP headers ``X-Auth-User`` and ``X-Auth_User`` (for example) both
+ normalize to the ``HTTP_X_AUTH_USER`` key in ``request.META``, you must
+ also check that your web server doesn't allow a spoofed header using
+ underscores in place of dashes.
+
+ This warning doesn't apply to ``RemoteUserMiddleware`` in its default
+ configuration with ``header = 'REMOTE_USER'``, since a key that doesn't
+ start with ``HTTP_`` in ``request.META`` can only be set by your WSGI
+ server, not directly from an HTTP request header.
+
If you need more control, you can create your own authentication backend
that inherits from :class:`~django.contrib.auth.backends.RemoteUserBackend` and
override one or more of its attributes and methods.
diff --git a/docs/releases/1.4.18.txt b/docs/releases/1.4.18.txt
index e5df185cfb..55256cfdf3 100644
--- a/docs/releases/1.4.18.txt
+++ b/docs/releases/1.4.18.txt
@@ -7,6 +7,30 @@ Django 1.4.18 release notes
Django 1.4.18 fixes several security issues in 1.4.17 as well as a regression
on Python 2.5 in the 1.4.17 release.
+WSGI header spoofing via underscore/dash conflation
+===================================================
+
+When HTTP headers are placed into the WSGI environ, they are normalized by
+converting to uppercase, converting all dashes to underscores, and prepending
+`HTTP_`. For instance, a header ``X-Auth-User`` would become
+``HTTP_X_AUTH_USER`` in the WSGI environ (and thus also in Django's
+``request.META`` dictionary).
+
+Unfortunately, this means that the WSGI environ cannot distinguish between
+headers containing dashes and headers containing underscores: ``X-Auth-User``
+and ``X-Auth_User`` both become ``HTTP_X_AUTH_USER``. This means that if a
+header is used in a security-sensitive way (for instance, passing
+authentication information along from a front-end proxy), even if the proxy
+carefully strips any incoming value for ``X-Auth-User``, an attacker may be
+able to provide an ``X-Auth_User`` header (with underscore) and bypass this
+protection.
+
+In order to prevent such attacks, both Nginx and Apache 2.4+ strip all headers
+containing underscores from incoming requests by default. Django's built-in
+development server now does the same. Django's development server is not
+recommended for production use, but matching the behavior of common production
+servers reduces the surface area for behavior changes during deployment.
+
Bugfixes
========
diff --git a/docs/releases/1.6.10.txt b/docs/releases/1.6.10.txt
index 72eb21e137..dafee70c8c 100644
--- a/docs/releases/1.6.10.txt
+++ b/docs/releases/1.6.10.txt
@@ -5,3 +5,27 @@ Django 1.6.10 release notes
*Under development*
Django 1.6.10 fixes several security issues in 1.6.9.
+
+WSGI header spoofing via underscore/dash conflation
+===================================================
+
+When HTTP headers are placed into the WSGI environ, they are normalized by
+converting to uppercase, converting all dashes to underscores, and prepending
+`HTTP_`. For instance, a header ``X-Auth-User`` would become
+``HTTP_X_AUTH_USER`` in the WSGI environ (and thus also in Django's
+``request.META`` dictionary).
+
+Unfortunately, this means that the WSGI environ cannot distinguish between
+headers containing dashes and headers containing underscores: ``X-Auth-User``
+and ``X-Auth_User`` both become ``HTTP_X_AUTH_USER``. This means that if a
+header is used in a security-sensitive way (for instance, passing
+authentication information along from a front-end proxy), even if the proxy
+carefully strips any incoming value for ``X-Auth-User``, an attacker may be
+able to provide an ``X-Auth_User`` header (with underscore) and bypass this
+protection.
+
+In order to prevent such attacks, both Nginx and Apache 2.4+ strip all headers
+containing underscores from incoming requests by default. Django's built-in
+development server now does the same. Django's development server is not
+recommended for production use, but matching the behavior of common production
+servers reduces the surface area for behavior changes during deployment.
diff --git a/docs/releases/1.7.3.txt b/docs/releases/1.7.3.txt
index 5e3cfcd84a..20d0b59457 100644
--- a/docs/releases/1.7.3.txt
+++ b/docs/releases/1.7.3.txt
@@ -6,7 +6,29 @@ Django 1.7.3 release notes
Django 1.7.3 fixes several security issues and bugs in 1.7.2.
+WSGI header spoofing via underscore/dash conflation
+===================================================
+When HTTP headers are placed into the WSGI environ, they are normalized by
+converting to uppercase, converting all dashes to underscores, and prepending
+`HTTP_`. For instance, a header ``X-Auth-User`` would become
+``HTTP_X_AUTH_USER`` in the WSGI environ (and thus also in Django's
+``request.META`` dictionary).
+
+Unfortunately, this means that the WSGI environ cannot distinguish between
+headers containing dashes and headers containing underscores: ``X-Auth-User``
+and ``X-Auth_User`` both become ``HTTP_X_AUTH_USER``. This means that if a
+header is used in a security-sensitive way (for instance, passing
+authentication information along from a front-end proxy), even if the proxy
+carefully strips any incoming value for ``X-Auth-User``, an attacker may be
+able to provide an ``X-Auth_User`` header (with underscore) and bypass this
+protection.
+
+In order to prevent such attacks, both Nginx and Apache 2.4+ strip all headers
+containing underscores from incoming requests by default. Django's built-in
+development server now does the same. Django's development server is not
+recommended for production use, but matching the behavior of common production
+servers reduces the surface area for behavior changes during deployment.
Bugfixes
========
diff --git a/tests/servers/test_basehttp.py b/tests/servers/test_basehttp.py
new file mode 100644
index 0000000000..6bca608d17
--- /dev/null
+++ b/tests/servers/test_basehttp.py
@@ -0,0 +1,67 @@
+import sys
+
+from django.core.servers.basehttp import WSGIRequestHandler
+from django.test import TestCase
+from django.utils.six import BytesIO, StringIO
+
+
+class Stub(object):
+ def __init__(self, **kwargs):
+ self.__dict__.update(kwargs)
+
+
+class WSGIRequestHandlerTestCase(TestCase):
+
+ def test_strips_underscore_headers(self):
+ """WSGIRequestHandler ignores headers containing underscores.
+
+ This follows the lead of nginx and Apache 2.4, and is to avoid
+ ambiguity between dashes and underscores in mapping to WSGI environ,
+ which can have security implications.
+ """
+ def test_app(environ, start_response):
+ """A WSGI app that just reflects its HTTP environ."""
+ start_response('200 OK', [])
+ http_environ_items = sorted(
+ '%s:%s' % (k, v) for k, v in environ.items()
+ if k.startswith('HTTP_')
+ )
+ yield (','.join(http_environ_items)).encode('utf-8')
+
+ rfile = BytesIO()
+ rfile.write(b"GET / HTTP/1.0\r\n")
+ rfile.write(b"Some-Header: good\r\n")
+ rfile.write(b"Some_Header: bad\r\n")
+ rfile.write(b"Other_Header: bad\r\n")
+ rfile.seek(0)
+
+ # WSGIRequestHandler closes the output file; we need to make this a
+ # no-op so we can still read its contents.
+ class UnclosableBytesIO(BytesIO):
+ def close(self):
+ pass
+
+ wfile = UnclosableBytesIO()
+
+ def makefile(mode, *a, **kw):
+ if mode == 'rb':
+ return rfile
+ elif mode == 'wb':
+ return wfile
+
+ request = Stub(makefile=makefile)
+ server = Stub(base_environ={}, get_app=lambda: test_app)
+
+ # We don't need to check stderr, but we don't want it in test output
+ old_stderr = sys.stderr
+ sys.stderr = StringIO()
+ try:
+ # instantiating a handler runs the request as side effect
+ WSGIRequestHandler(request, '192.168.0.2', server)
+ finally:
+ sys.stderr = old_stderr
+
+ wfile.seek(0)
+ body = list(wfile.readlines())[-1]
+
+ self.assertEqual(body, b'HTTP_SOME_HEADER:good')