Code

Ticket #13751: openredirect.diff

File openredirect.diff, 8.9 KB (added by flier, 4 years ago)
Line 
1Index: django/http/__init__.py
2===================================================================
3--- django/http/__init__.py     (revision 13402)
4+++ django/http/__init__.py     (working copy)
5@@ -1,9 +1,10 @@
6 import os
7 import re
8+import logging
9 from Cookie import BaseCookie, SimpleCookie, CookieError
10 from pprint import pformat
11 from urllib import urlencode
12-from urlparse import urljoin
13+from urlparse import urljoin, urlparse
14 try:
15     # The mod_python version is more efficient, so try importing it first.
16     from mod_python.util import parse_qsl
17@@ -189,7 +190,7 @@
18         for key, value in dict.items(self):
19             dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo))
20         return result
21-   
22+
23     def setlist(self, key, list_):
24         self._assert_mutable()
25         key = str_to_unicode(key, self.encoding)
26@@ -432,10 +433,26 @@
27 class HttpResponseRedirect(HttpResponse):
28     status_code = 302
29 
30-    def __init__(self, redirect_to):
31+    def __init__(self, redirect_to, whitelist=[], fallback_to=None):
32         HttpResponse.__init__(self)
33         self['Location'] = iri_to_uri(redirect_to)
34 
35+        if urlparse(self['Location']).scheme:
36+            effective_whitelist = whitelist + getattr(settings, 'REDIRECT_WHITELIST', [])
37+
38+            matched = False
39+
40+            for pattern in effective_whitelist:
41+                matched = re.compile(pattern).match(self['Location'])
42+
43+                if matched:
44+                    break
45+
46+            if not matched:
47+                logging.warn("found open redirect attack to %s", self['Location'])
48+
49+                self['Location'] = fallback_to or settings.LOGIN_REDIRECT_URL
50+
51 class HttpResponsePermanentRedirect(HttpResponse):
52     status_code = 301
53 
54@@ -493,4 +510,3 @@
55         return unicode(s, encoding, 'replace')
56     else:
57         return s
58-
59Index: tests/regressiontests/httpwrappers/tests.py
60===================================================================
61--- tests/regressiontests/httpwrappers/tests.py (revision 13402)
62+++ tests/regressiontests/httpwrappers/tests.py (working copy)
63@@ -1,7 +1,8 @@
64 import copy
65 import pickle
66 import unittest
67-from django.http import QueryDict, HttpResponse, CompatCookie, BadHeaderError
68+from django.conf import settings
69+from django.http import QueryDict, HttpResponse, CompatCookie, BadHeaderError, HttpResponseRedirect
70 
71 class QueryDictTests(unittest.TestCase):
72     def test_missing_key(self):
73@@ -17,7 +18,7 @@
74         self.assertRaises(AttributeError, q.pop, 'foo')
75         self.assertRaises(AttributeError, q.popitem)
76         self.assertRaises(AttributeError, q.clear)
77-       
78+
79     def test_immutable_get_with_default(self):
80         q = QueryDict('')
81         self.assertEqual(q.get('foo', 'default'), 'default')
82@@ -34,7 +35,7 @@
83         self.assertEqual(q.values(), [])
84         self.assertEqual(len(q), 0)
85         self.assertEqual(q.urlencode(), '')
86-       
87+
88     def test_single_key_value(self):
89         """Test QueryDict with one key/value pair"""
90 
91@@ -47,7 +48,7 @@
92         self.assertEqual(q.get('bar', 'default'), 'default')
93         self.assertEqual(q.getlist('foo'), ['bar'])
94         self.assertEqual(q.getlist('bar'), [])
95-       
96+
97         self.assertRaises(AttributeError, q.setlist, 'foo', ['bar'])
98         self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
99 
100@@ -67,16 +68,16 @@
101         self.assertRaises(AttributeError, q.popitem)
102         self.assertRaises(AttributeError, q.clear)
103         self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
104-       
105+
106         self.assertEqual(q.urlencode(), 'foo=bar')
107-       
108+
109     def test_mutable_copy(self):
110         """A copy of a QueryDict is mutable."""
111         q = QueryDict('').copy()
112         self.assertRaises(KeyError, q.__getitem__, "foo")
113         q['name'] = 'john'
114         self.assertEqual(q['name'], 'john')
115-       
116+
117     def test_mutable_delete(self):
118         q = QueryDict('').copy()
119         q['name'] = 'john'
120@@ -126,20 +127,20 @@
121         """Test QueryDict with two key/value pairs with same keys."""
122 
123         q = QueryDict('vote=yes&vote=no')
124-       
125+
126         self.assertEqual(q['vote'], u'no')
127         self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar')
128-               
129+
130         self.assertEqual(q.get('vote', 'default'), u'no')
131         self.assertEqual(q.get('foo', 'default'), 'default')
132         self.assertEqual(q.getlist('vote'), [u'yes', u'no'])
133         self.assertEqual(q.getlist('foo'), [])
134-       
135+
136         self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
137         self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
138         self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
139 
140-        self.assertEqual(q.has_key('vote'), True)       
141+        self.assertEqual(q.has_key('vote'), True)
142         self.assertEqual('vote' in q, True)
143         self.assertEqual(q.has_key('foo'), False)
144         self.assertEqual('foo' in q, False)
145@@ -148,23 +149,23 @@
146         self.assertEqual(q.keys(), [u'vote'])
147         self.assertEqual(q.values(), [u'no'])
148         self.assertEqual(len(q), 1)
149-       
150+
151         self.assertRaises(AttributeError, q.update, {'foo': 'bar'})
152         self.assertRaises(AttributeError, q.pop, 'foo')
153         self.assertRaises(AttributeError, q.popitem)
154         self.assertRaises(AttributeError, q.clear)
155         self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
156         self.assertRaises(AttributeError, q.__delitem__, 'vote')
157-       
158+
159     def test_invalid_input_encoding(self):
160         """
161         QueryDicts must be able to handle invalid input encoding (in this
162         case, bad UTF-8 encoding).
163         """
164         q = QueryDict('foo=bar&foo=\xff')
165-        self.assertEqual(q['foo'], u'\ufffd')       
166+        self.assertEqual(q['foo'], u'\ufffd')
167         self.assertEqual(q.getlist('foo'), [u'bar', u'\ufffd'])
168-   
169+
170     def test_pickle(self):
171         q = QueryDict('')
172         q1 = pickle.loads(pickle.dumps(q, 2))
173@@ -172,7 +173,7 @@
174         q = QueryDict('a=b&c=d')
175         q1 = pickle.loads(pickle.dumps(q, 2))
176         self.assertEqual(q == q1, True)
177-        q = QueryDict('a=b&c=d&a=1')
178+        q = QueryDict('a=b&c=d&a=1')
179         q1 = pickle.loads(pickle.dumps(q, 2))
180         self.assertEqual(q == q1 , True)
181 
182@@ -181,21 +182,21 @@
183         x = QueryDict("a=1&a=2", mutable=True)
184         y = QueryDict("a=3&a=4")
185         x.update(y)
186-        self.assertEqual(x.getlist('a'), [u'1', u'2', u'3', u'4'])   
187+        self.assertEqual(x.getlist('a'), [u'1', u'2', u'3', u'4'])
188 
189     def test_non_default_encoding(self):
190         """#13572 - QueryDict with a non-default encoding"""
191-        q = QueryDict('sbb=one', encoding='rot_13')
192+        q = QueryDict('sbb=one', encoding='rot_13')
193         self.assertEqual(q.encoding , 'rot_13' )
194         self.assertEqual(q.items() , [(u'foo', u'bar')] )
195         self.assertEqual(q.urlencode() , 'sbb=one' )
196-        q = q.copy()
197+        q = q.copy()
198         self.assertEqual(q.encoding , 'rot_13' )
199         self.assertEqual(q.items() , [(u'foo', u'bar')] )
200         self.assertEqual(q.urlencode() , 'sbb=one' )
201         self.assertEqual(copy.copy(q).encoding , 'rot_13' )
202         self.assertEqual(copy.deepcopy(q).encoding , 'rot_13')
203-       
204+
205 class HttpResponseTests(unittest.TestCase):
206     def test_unicode_headers(self):
207         r = HttpResponse()
208@@ -203,16 +204,16 @@
209         # If we insert a unicode value it will be converted to an ascii
210         r['value'] = u'test value'
211         self.failUnless(isinstance(r['value'], str))
212-       
213+
214         # An error is raised When a unicode object with non-ascii is assigned.
215         self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', u't\xebst value')
216-       
217-        # The response also converts unicode keys to strings.)     
218+
219+        # The response also converts unicode keys to strings.)
220         r[u'test'] = 'testing key'
221         l = list(r.items())
222         l.sort()
223         self.assertEqual(l[1], ('test', 'testing key'))
224-       
225+
226         # It will also raise errors for keys with non-ascii data.
227         self.assertRaises(UnicodeEncodeError, r.__setitem__, u't\xebst key', 'value')
228 
229@@ -254,3 +255,14 @@
230         c2 = CompatCookie()
231         c2.load(c.output())
232         self.assertEqual(c['test'].value, c2['test'].value)
233+
234+class HttpResponseRedirectTests(unittest.TestCase):
235+    def test_open_redirect(self):
236+        self.assertEqual('/test', HttpResponseRedirect('/test')['Location'])
237+        self.assertEqual(settings.LOGIN_REDIRECT_URL, HttpResponseRedirect('http://djangoproject.com')['Location'])
238+
239+        r = HttpResponseRedirect('http://djangoproject.com', whitelist=[r'.*'])
240+        self.assertEqual('http://djangoproject.com', r['Location'])
241+
242+        setattr(settings, 'REDIRECT_WHITELIST', [r'.*'])
243+        self.assertEqual('http://djangoproject.com', HttpResponseRedirect('http://djangoproject.com')['Location'])