Skip to content

Commit 4129201

Browse files
committed
Fixed a security issue in http redirects. Disclosure and new release forthcoming.
1 parent b1d4634 commit 4129201

File tree

2 files changed

+30
-13
lines changed

2 files changed

+30
-13
lines changed

django/http/__init__.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
from io import BytesIO
1212
from pprint import pformat
1313
try:
14-
from urllib.parse import quote, parse_qsl, urlencode, urljoin
14+
from urllib.parse import quote, parse_qsl, urlencode, urljoin, urlparse
1515
except ImportError: # Python 2
1616
from urllib import quote, urlencode
17-
from urlparse import parse_qsl, urljoin
17+
from urlparse import parse_qsl, urljoin, urlparse
1818

1919
from django.utils.six.moves import http_cookies
2020
# Some versions of Python 2.7 and later won't need this encoding bug fix:
@@ -80,7 +80,7 @@ def _BaseCookie__set(self, key, real_value, coded_value):
8080

8181
from django.conf import settings
8282
from django.core import signing
83-
from django.core.exceptions import ImproperlyConfigured
83+
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
8484
from django.core.files import uploadhandler
8585
from django.http.multipartparser import MultiPartParser
8686
from django.http.utils import *
@@ -689,20 +689,22 @@ def tell(self):
689689
raise Exception("This %s instance cannot tell its position" % self.__class__)
690690
return sum([len(chunk) for chunk in self])
691691

692-
class HttpResponseRedirect(HttpResponse):
693-
status_code = 302
692+
class HttpResponseRedirectBase(HttpResponse):
693+
allowed_schemes = ['http', 'https', 'ftp']
694694

695695
def __init__(self, redirect_to):
696-
super(HttpResponseRedirect, self).__init__()
696+
parsed = urlparse(redirect_to)
697+
if parsed.scheme and parsed.scheme not in self.allowed_schemes:
698+
raise SuspiciousOperation("Unsafe redirect to URL with protocol '%s'" % parsed.scheme)
699+
super(HttpResponseRedirectBase, self).__init__()
697700
self['Location'] = iri_to_uri(redirect_to)
701+
702+
class HttpResponseRedirect(HttpResponseRedirectBase):
703+
status_code = 302
698704

699-
class HttpResponsePermanentRedirect(HttpResponse):
705+
class HttpResponsePermanentRedirect(HttpResponseRedirectBase):
700706
status_code = 301
701707

702-
def __init__(self, redirect_to):
703-
super(HttpResponsePermanentRedirect, self).__init__()
704-
self['Location'] = iri_to_uri(redirect_to)
705-
706708
class HttpResponseNotModified(HttpResponse):
707709
status_code = 304
708710

tests/regressiontests/httpwrappers/tests.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
import copy
55
import pickle
66

7-
from django.http import (QueryDict, HttpResponse, SimpleCookie, BadHeaderError,
8-
parse_cookie)
7+
from django.core.exceptions import SuspiciousOperation
8+
from django.http import (QueryDict, HttpResponse, HttpResponseRedirect,
9+
HttpResponsePermanentRedirect,
10+
SimpleCookie, BadHeaderError,
11+
parse_cookie)
912
from django.utils import unittest
1013

1114

@@ -309,6 +312,18 @@ def test_file_interface(self):
309312
r = HttpResponse(['abc'])
310313
self.assertRaises(Exception, r.write, 'def')
311314

315+
def test_unsafe_redirect(self):
316+
bad_urls = [
317+
'data:text/html,<script>window.alert("xss")</script>',
318+
'mailto:test@example.com',
319+
'file:///etc/passwd',
320+
]
321+
for url in bad_urls:
322+
self.assertRaises(SuspiciousOperation,
323+
HttpResponseRedirect, url)
324+
self.assertRaises(SuspiciousOperation,
325+
HttpResponsePermanentRedirect, url)
326+
312327

313328
class CookieTests(unittest.TestCase):
314329
def test_encode(self):

0 commit comments

Comments
 (0)