Ticket #13751: openredirect.diff

File openredirect.diff, 8.9 KB (added by Flier Lu, 14 years ago)
  • django/http/__init__.py

     
    11import os
    22import re
     3import logging
    34from Cookie import BaseCookie, SimpleCookie, CookieError
    45from pprint import pformat
    56from urllib import urlencode
    6 from urlparse import urljoin
     7from urlparse import urljoin, urlparse
    78try:
    89    # The mod_python version is more efficient, so try importing it first.
    910    from mod_python.util import parse_qsl
     
    189190        for key, value in dict.items(self):
    190191            dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo))
    191192        return result
    192    
     193
    193194    def setlist(self, key, list_):
    194195        self._assert_mutable()
    195196        key = str_to_unicode(key, self.encoding)
     
    432433class HttpResponseRedirect(HttpResponse):
    433434    status_code = 302
    434435
    435     def __init__(self, redirect_to):
     436    def __init__(self, redirect_to, whitelist=[], fallback_to=None):
    436437        HttpResponse.__init__(self)
    437438        self['Location'] = iri_to_uri(redirect_to)
    438439
     440        if urlparse(self['Location']).scheme:
     441            effective_whitelist = whitelist + getattr(settings, 'REDIRECT_WHITELIST', [])
     442
     443            matched = False
     444
     445            for pattern in effective_whitelist:
     446                matched = re.compile(pattern).match(self['Location'])
     447
     448                if matched:
     449                    break
     450
     451            if not matched:
     452                logging.warn("found open redirect attack to %s", self['Location'])
     453
     454                self['Location'] = fallback_to or settings.LOGIN_REDIRECT_URL
     455
    439456class HttpResponsePermanentRedirect(HttpResponse):
    440457    status_code = 301
    441458
     
    493510        return unicode(s, encoding, 'replace')
    494511    else:
    495512        return s
    496 
  • tests/regressiontests/httpwrappers/tests.py

     
    11import copy
    22import pickle
    33import unittest
    4 from django.http import QueryDict, HttpResponse, CompatCookie, BadHeaderError
     4from django.conf import settings
     5from django.http import QueryDict, HttpResponse, CompatCookie, BadHeaderError, HttpResponseRedirect
    56
    67class QueryDictTests(unittest.TestCase):
    78    def test_missing_key(self):
     
    1718        self.assertRaises(AttributeError, q.pop, 'foo')
    1819        self.assertRaises(AttributeError, q.popitem)
    1920        self.assertRaises(AttributeError, q.clear)
    20        
     21
    2122    def test_immutable_get_with_default(self):
    2223        q = QueryDict('')
    2324        self.assertEqual(q.get('foo', 'default'), 'default')
     
    3435        self.assertEqual(q.values(), [])
    3536        self.assertEqual(len(q), 0)
    3637        self.assertEqual(q.urlencode(), '')
    37        
     38
    3839    def test_single_key_value(self):
    3940        """Test QueryDict with one key/value pair"""
    4041
     
    4748        self.assertEqual(q.get('bar', 'default'), 'default')
    4849        self.assertEqual(q.getlist('foo'), ['bar'])
    4950        self.assertEqual(q.getlist('bar'), [])
    50        
     51
    5152        self.assertRaises(AttributeError, q.setlist, 'foo', ['bar'])
    5253        self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
    5354
     
    6768        self.assertRaises(AttributeError, q.popitem)
    6869        self.assertRaises(AttributeError, q.clear)
    6970        self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
    70        
     71
    7172        self.assertEqual(q.urlencode(), 'foo=bar')
    72        
     73
    7374    def test_mutable_copy(self):
    7475        """A copy of a QueryDict is mutable."""
    7576        q = QueryDict('').copy()
    7677        self.assertRaises(KeyError, q.__getitem__, "foo")
    7778        q['name'] = 'john'
    7879        self.assertEqual(q['name'], 'john')
    79        
     80
    8081    def test_mutable_delete(self):
    8182        q = QueryDict('').copy()
    8283        q['name'] = 'john'
     
    126127        """Test QueryDict with two key/value pairs with same keys."""
    127128
    128129        q = QueryDict('vote=yes&vote=no')
    129        
     130
    130131        self.assertEqual(q['vote'], u'no')
    131132        self.assertRaises(AttributeError, q.__setitem__, 'something', 'bar')
    132                
     133
    133134        self.assertEqual(q.get('vote', 'default'), u'no')
    134135        self.assertEqual(q.get('foo', 'default'), 'default')
    135136        self.assertEqual(q.getlist('vote'), [u'yes', u'no'])
    136137        self.assertEqual(q.getlist('foo'), [])
    137        
     138
    138139        self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
    139140        self.assertRaises(AttributeError, q.setlist, 'foo', ['bar', 'baz'])
    140141        self.assertRaises(AttributeError, q.appendlist, 'foo', ['bar'])
    141142
    142         self.assertEqual(q.has_key('vote'), True)       
     143        self.assertEqual(q.has_key('vote'), True)
    143144        self.assertEqual('vote' in q, True)
    144145        self.assertEqual(q.has_key('foo'), False)
    145146        self.assertEqual('foo' in q, False)
     
    148149        self.assertEqual(q.keys(), [u'vote'])
    149150        self.assertEqual(q.values(), [u'no'])
    150151        self.assertEqual(len(q), 1)
    151        
     152
    152153        self.assertRaises(AttributeError, q.update, {'foo': 'bar'})
    153154        self.assertRaises(AttributeError, q.pop, 'foo')
    154155        self.assertRaises(AttributeError, q.popitem)
    155156        self.assertRaises(AttributeError, q.clear)
    156157        self.assertRaises(AttributeError, q.setdefault, 'foo', 'bar')
    157158        self.assertRaises(AttributeError, q.__delitem__, 'vote')
    158        
     159
    159160    def test_invalid_input_encoding(self):
    160161        """
    161162        QueryDicts must be able to handle invalid input encoding (in this
    162163        case, bad UTF-8 encoding).
    163164        """
    164165        q = QueryDict('foo=bar&foo=\xff')
    165         self.assertEqual(q['foo'], u'\ufffd')       
     166        self.assertEqual(q['foo'], u'\ufffd')
    166167        self.assertEqual(q.getlist('foo'), [u'bar', u'\ufffd'])
    167    
     168
    168169    def test_pickle(self):
    169170        q = QueryDict('')
    170171        q1 = pickle.loads(pickle.dumps(q, 2))
     
    172173        q = QueryDict('a=b&c=d')
    173174        q1 = pickle.loads(pickle.dumps(q, 2))
    174175        self.assertEqual(q == q1, True)
    175         q = QueryDict('a=b&c=d&a=1') 
     176        q = QueryDict('a=b&c=d&a=1')
    176177        q1 = pickle.loads(pickle.dumps(q, 2))
    177178        self.assertEqual(q == q1 , True)
    178179
     
    181182        x = QueryDict("a=1&a=2", mutable=True)
    182183        y = QueryDict("a=3&a=4")
    183184        x.update(y)
    184         self.assertEqual(x.getlist('a'), [u'1', u'2', u'3', u'4'])   
     185        self.assertEqual(x.getlist('a'), [u'1', u'2', u'3', u'4'])
    185186
    186187    def test_non_default_encoding(self):
    187188        """#13572 - QueryDict with a non-default encoding"""
    188         q = QueryDict('sbb=one', encoding='rot_13') 
     189        q = QueryDict('sbb=one', encoding='rot_13')
    189190        self.assertEqual(q.encoding , 'rot_13' )
    190191        self.assertEqual(q.items() , [(u'foo', u'bar')] )
    191192        self.assertEqual(q.urlencode() , 'sbb=one' )
    192         q = q.copy() 
     193        q = q.copy()
    193194        self.assertEqual(q.encoding , 'rot_13' )
    194195        self.assertEqual(q.items() , [(u'foo', u'bar')] )
    195196        self.assertEqual(q.urlencode() , 'sbb=one' )
    196197        self.assertEqual(copy.copy(q).encoding , 'rot_13' )
    197198        self.assertEqual(copy.deepcopy(q).encoding , 'rot_13')
    198        
     199
    199200class HttpResponseTests(unittest.TestCase):
    200201    def test_unicode_headers(self):
    201202        r = HttpResponse()
     
    203204        # If we insert a unicode value it will be converted to an ascii
    204205        r['value'] = u'test value'
    205206        self.failUnless(isinstance(r['value'], str))
    206        
     207
    207208        # An error is raised When a unicode object with non-ascii is assigned.
    208209        self.assertRaises(UnicodeEncodeError, r.__setitem__, 'value', u't\xebst value')
    209        
    210         # The response also converts unicode keys to strings.)     
     210
     211        # The response also converts unicode keys to strings.)
    211212        r[u'test'] = 'testing key'
    212213        l = list(r.items())
    213214        l.sort()
    214215        self.assertEqual(l[1], ('test', 'testing key'))
    215        
     216
    216217        # It will also raise errors for keys with non-ascii data.
    217218        self.assertRaises(UnicodeEncodeError, r.__setitem__, u't\xebst key', 'value')
    218219
     
    254255        c2 = CompatCookie()
    255256        c2.load(c.output())
    256257        self.assertEqual(c['test'].value, c2['test'].value)
     258
     259class HttpResponseRedirectTests(unittest.TestCase):
     260    def test_open_redirect(self):
     261        self.assertEqual('/test', HttpResponseRedirect('/test')['Location'])
     262        self.assertEqual(settings.LOGIN_REDIRECT_URL, HttpResponseRedirect('http://djangoproject.com')['Location'])
     263
     264        r = HttpResponseRedirect('http://djangoproject.com', whitelist=[r'.*'])
     265        self.assertEqual('http://djangoproject.com', r['Location'])
     266
     267        setattr(settings, 'REDIRECT_WHITELIST', [r'.*'])
     268        self.assertEqual('http://djangoproject.com', HttpResponseRedirect('http://djangoproject.com')['Location'])
Back to Top