Moderate severityNVD Advisory· Published Jan 16, 2015· Updated May 6, 2026
CVE-2015-0219
CVE-2015-0219
Description
Django before 1.4.18, 1.6.x before 1.6.10, and 1.7.x before 1.7.3 allows remote attackers to spoof WSGI headers by using an _ (underscore) character instead of a - (dash) character in an HTTP header, as demonstrated by an X-Auth_User header.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | < 1.4.18 | 1.4.18 |
DjangoPyPI | >= 1.6, < 1.6.10 | 1.6.10 |
DjangoPyPI | >= 1.7, < 1.7.3 | 1.7.3 |
Affected products
14cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*+ 13 more
- cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*range: <=1.4.17
- cpe:2.3:a:djangoproject:django:1.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.3:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.7:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.8:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.9:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7.2:*:*:*:*:*:*:*
Patches
34f6fffc1dc42[1.4.x] Stripped headers containing underscores to prevent spoofing in WSGI environ.
3 files changed · +102 −0
django/core/servers/basehttp.py+11 −0 modified@@ -199,6 +199,17 @@ def log_message(self, format, *args): 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() + class AdminMediaHandler(handlers.StaticFilesHandler): """
docs/releases/1.4.18.txt+24 −0 modified@@ -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 ========
tests/regressiontests/servers/servers/test_basehttp.py+67 −0 added@@ -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')
d7597b31d5c0[1.6.x] Stripped headers containing underscores to prevent spoofing in WSGI environ.
5 files changed · +142 −0
django/core/servers/basehttp.py+11 −0 modified@@ -157,6 +157,17 @@ def log_message(self, format, *args): 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)
docs/howto/auth-remote-user.txt+16 −0 modified@@ -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.
docs/releases/1.4.18.txt+24 −0 modified@@ -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 ========
docs/releases/1.6.10.txt+24 −0 modified@@ -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.
tests/servers/test_basehttp.py+67 −0 added@@ -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')
41b4bc73ee0d[1.7.x] Stripped headers containing underscores to prevent spoofing in WSGI environ.
6 files changed · +165 −1
django/core/servers/basehttp.py+11 −0 modified@@ -155,6 +155,17 @@ def log_message(self, format, *args): 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)
docs/howto/auth-remote-user.txt+16 −0 modified@@ -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.
docs/releases/1.4.18.txt+24 −0 modified@@ -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 ========
docs/releases/1.6.10.txt+24 −0 modified@@ -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.
docs/releases/1.7.3.txt+23 −1 modified@@ -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 ========
tests/servers/test_basehttp.py+67 −0 added@@ -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')
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
26- www.ubuntu.com/usn/USN-2469-1nvdPatchVendor AdvisoryWEB
- www.djangoproject.com/weblog/2015/jan/13/security/nvdExploitPatchVendor Advisory
- github.com/advisories/GHSA-7qfw-j7hp-v45gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2015-0219ghsaADVISORY
- advisories.mageia.org/MGASA-2015-0026.htmlnvdWEB
- lists.fedoraproject.org/pipermail/package-announce/2015-January/148485.htmlnvdWEB
- lists.fedoraproject.org/pipermail/package-announce/2015-January/148608.htmlnvdWEB
- lists.fedoraproject.org/pipermail/package-announce/2015-January/148696.htmlnvdWEB
- lists.opensuse.org/opensuse-updates/2015-04/msg00001.htmlnvdWEB
- lists.opensuse.org/opensuse-updates/2015-09/msg00035.htmlnvdWEB
- github.com/django/daphne/blob/e49c39a4e5fac8ec170dd653641a9e90844fd3f1/daphne/http_protocol.pyghsaWEB
- github.com/django/django/commit/41b4bc73ee0da7b2e09f4af47fc1fd21144c710fghsaWEB
- github.com/django/django/commit/4f6fffc1dc429f1ad428ecf8e6620739e8837450ghsaWEB
- github.com/django/django/commit/d7597b31d5c03106eeba4be14a33b32a5e25f4eeghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2015-4.yamlghsaWEB
- web.archive.org/web/20150128111656/http://secunia.com/advisories/62285ghsaWEB
- web.archive.org/web/20150128111656/http://secunia.com/advisories/62309ghsaWEB
- web.archive.org/web/20150523054951/http://www.mandriva.com/en/support/security/advisories/advisory/MDVSA-2015:109/ghsaWEB
- web.archive.org/web/20150523054953/http://www.mandriva.com/en/support/security/advisories/advisory/MDVSA-2015:036/ghsaWEB
- web.archive.org/web/20151104201446/http://secunia.com/advisories/62718ghsaWEB
- www.djangoproject.com/weblog/2015/jan/13/securityghsaWEB
- secunia.com/advisories/62285nvd
- secunia.com/advisories/62309nvd
- secunia.com/advisories/62718nvd
- www.mandriva.com/security/advisoriesnvd
- www.mandriva.com/security/advisoriesnvd
News mentions
0No linked articles in our index yet.