티스토리 수익 글 보기

티스토리 수익 글 보기

[1.5.x] Fixed a remote code execution vulnerabilty in URL reversing. · django/django@2a5bcb6 · GitHub
Skip to content

Commit 2a5bcb6

Browse files
committed
[1.5.x] Fixed a remote code execution vulnerabilty in URL reversing.
Thanks Benjamin Bach for the report and initial patch. This is a security fix; disclosure to follow shortly. Backport of 8b93b31 from master
1 parent d6c685c commit 2a5bcb6

File tree

5 files changed

+51
2
lines changed

5 files changed

+51
2
lines changed

django/core/urlresolvers.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name
244244
self._reverse_dict = {}
245245
self._namespace_dict = {}
246246
self._app_dict = {}
247+
# set of dotted paths to all functions and classes that are used in
248+
# urlpatterns
249+
self._callback_strs = set()
250+
self._populated = False
247251

248252
def __repr__(self):
249253
if isinstance(self.urlconf_name, list) and len(self.urlconf_name):
@@ -261,6 +265,15 @@ def _populate(self):
261265
apps = {}
262266
language_code = get_language()
263267
for pattern in reversed(self.url_patterns):
268+
if hasattr(pattern, '_callback_str'):
269+
self._callback_strs.add(pattern._callback_str)
270+
elif hasattr(pattern, '_callback'):
271+
callback = pattern._callback
272+
if not hasattr(callback, '__name__'):
273+
lookup_str = callback.__module__ + "." + callback.__class__.__name__
274+
else:
275+
lookup_str = callback.__module__ + "." + callback.__name__
276+
self._callback_strs.add(lookup_str)
264277
p_pattern = pattern.regex.pattern
265278
if p_pattern.startswith('^'):
266279
p_pattern = p_pattern[1:]
@@ -281,6 +294,7 @@ def _populate(self):
281294
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
282295
for app_name, namespace_list in pattern.app_dict.items():
283296
apps.setdefault(app_name, []).extend(namespace_list)
297+
self._callback_strs.update(pattern._callback_strs)
284298
else:
285299
bits = normalize(p_pattern)
286300
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
@@ -289,6 +303,7 @@ def _populate(self):
289303
self._reverse_dict[language_code] = lookups
290304
self._namespace_dict[language_code] = namespaces
291305
self._app_dict[language_code] = apps
306+
self._populated = True
292307

293308
@property
294309
def reverse_dict(self):
@@ -375,8 +390,13 @@ def reverse(self, lookup_view, *args, **kwargs):
375390
def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
376391
if args and kwargs:
377392
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
393+
394+
if not self._populated:
395+
self._populate()
396+
378397
try:
379-
lookup_view = get_callable(lookup_view, True)
398+
if lookup_view in self._callback_strs:
399+
lookup_view = get_callable(lookup_view, True)
380400
except (ImportError, AttributeError) as e:
381401
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
382402
possibilities = self.reverse_dict.getlist(lookup_view)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
def view(request):
2+
"""Stub view"""
3+
pass

tests/regressiontests/urlpatterns_reverse/tests.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
# -*- coding: utf-8 -*-
12
"""
23
Unit tests for reverse URL lookups.
34
"""
45
from __future__ import absolute_import, unicode_literals
56

7+
import sys
8+
69
from django.conf import settings
710
from django.contrib.auth.models import User
811
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
@@ -290,6 +293,25 @@ def test_redirect_to_url(self):
290293
self.assertEqual(res['Location'], '/foo/')
291294
res = redirect('http://example.com/')
292295
self.assertEqual(res['Location'], 'http://example.com/')
296+
# Assert that we can redirect using UTF-8 strings
297+
res = redirect('/æøå/abc/')
298+
self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5/abc/')
299+
# Assert that no imports are attempted when dealing with a relative path
300+
# (previously, the below would resolve in a UnicodeEncodeError from __import__ )
301+
res = redirect('/æøå.abc/')
302+
self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5.abc/')
303+
res = redirect('os.path')
304+
self.assertEqual(res['Location'], 'os.path')
305+
306+
def test_no_illegal_imports(self):
307+
# modules that are not listed in urlpatterns should not be importable
308+
redirect("urlpatterns_reverse.nonimported_module.view")
309+
self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules)
310+
311+
def test_reverse_by_path_nested(self):
312+
# Views that are added to urlpatterns using include() should be
313+
# reversable by doted path.
314+
self.assertEqual(reverse('regressiontests.urlpatterns_reverse.views.nested_view'), '/includes/nested_path/')
293315

294316
def test_redirect_view_object(self):
295317
from .views import absolute_kwargs_view
@@ -559,4 +581,3 @@ def test_view_loading(self):
559581
# swallow it.
560582
self.assertRaises(AttributeError, get_callable,
561583
'regressiontests.urlpatterns_reverse.views_broken.i_am_broken')
562-

tests/regressiontests/urlpatterns_reverse/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
other_patterns = patterns('',
99
url(r'non_path_include/$', empty_view, name='non_path_include'),
10+
url(r'nested_path/$', 'regressiontests.urlpatterns_reverse.views.nested_view'),
1011
)
1112

1213
urlpatterns = patterns('',

tests/regressiontests/urlpatterns_reverse/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def absolute_kwargs_view(request, arg1=1, arg2=2):
1616
def defaults_view(request, arg1, arg2):
1717
pass
1818

19+
def nested_view(request):
20+
pass
21+
22+
1923
def erroneous_view(request):
2024
import non_existent
2125

0 commit comments

Comments
 (0)