1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
|
Author: Carl Meyer <carl@oddbird.net>
Date: Wed Sep 10 11:06:19 2014 -0600
Subject: 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.
Patch further backported to Django 1.2 by Raphaƫl Hertzog.
Origin: backport, https://github.com/django/django/commit/d7597b31d5c03106eeba4be14a33b32a5e25f4ee
Bug: https://code.djangoproject.com/ticket/24239
--- a/django/core/servers/basehttp.py
+++ b/django/core/servers/basehttp.py
@@ -583,6 +583,14 @@ class WSGIRequestHandler(BaseHTTPRequest
if length:
env['CONTENT_LENGTH'] = length
+ # 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]
+
for h in self.headers.headers:
k,v = h.split(':',1)
k=k.replace('-','_').upper(); v=v.strip()
--- a/tests/regressiontests/servers/tests.py
+++ b/tests/regressiontests/servers/tests.py
@@ -3,11 +3,13 @@ Tests for django.core.servers.
"""
import os
+import sys
+from StringIO import StringIO
import django
from django.test import TestCase
from django.core.handlers.wsgi import WSGIHandler
-from django.core.servers.basehttp import AdminMediaHandler
+from django.core.servers.basehttp import AdminMediaHandler, WSGIRequestHandler
class AdminMediaHandlerTests(TestCase):
@@ -65,3 +67,65 @@ class AdminMediaHandlerTests(TestCase):
continue
self.fail('URL: %s should have caused a ValueError exception.'
% url)
+
+
+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 = StringIO()
+ 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 UnclosableStringIO(StringIO):
+ def close(self):
+ pass
+
+ wfile = UnclosableStringIO()
+
+ 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')
|