티스토리 수익 글 보기

티스토리 수익 글 보기

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

Commit 5467405

Browse files
committed
[1.7.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 0bd913a commit 5467405

File tree

5 files changed

+49
1
lines changed

5 files changed

+49
1
lines changed

django/core/urlresolvers.py

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

249253
def __repr__(self):
250254
if isinstance(self.urlconf_name, list) and len(self.urlconf_name):
@@ -262,6 +266,15 @@ def _populate(self):
262266
apps = {}
263267
language_code = get_language()
264268
for pattern in reversed(self.url_patterns):
269+
if hasattr(pattern, '_callback_str'):
270+
self._callback_strs.add(pattern._callback_str)
271+
elif hasattr(pattern, '_callback'):
272+
callback = pattern._callback
273+
if not hasattr(callback, '__name__'):
274+
lookup_str = callback.__module__ + "." + callback.__class__.__name__
275+
else:
276+
lookup_str = callback.__module__ + "." + callback.__name__
277+
self._callback_strs.add(lookup_str)
265278
p_pattern = pattern.regex.pattern
266279
if p_pattern.startswith('^'):
267280
p_pattern = p_pattern[1:]
@@ -280,6 +293,7 @@ def _populate(self):
280293
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
281294
for app_name, namespace_list in pattern.app_dict.items():
282295
apps.setdefault(app_name, []).extend(namespace_list)
296+
self._callback_strs.update(pattern._callback_strs)
283297
else:
284298
bits = normalize(p_pattern)
285299
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
@@ -288,6 +302,7 @@ def _populate(self):
288302
self._reverse_dict[language_code] = lookups
289303
self._namespace_dict[language_code] = namespaces
290304
self._app_dict[language_code] = apps
305+
self._populated = True
291306

292307
@property
293308
def reverse_dict(self):
@@ -387,8 +402,12 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
387402
text_args = [force_text(v) for v in args]
388403
text_kwargs = dict((k, force_text(v)) for (k, v) in kwargs.items())
389404

405+
if not self._populated:
406+
self._populate()
407+
390408
try:
391-
lookup_view = get_callable(lookup_view, True)
409+
if lookup_view in self._callback_strs:
410+
lookup_view = get_callable(lookup_view, True)
392411
except (ImportError, AttributeError) as e:
393412
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
394413
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/urlpatterns_reverse/tests.py

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

7+
import sys
68
import unittest
79

810
from django.contrib.auth.models import User
@@ -355,6 +357,25 @@ def test_redirect_to_url(self):
355357
self.assertEqual(res.url, '/foo/')
356358
res = redirect('http://example.com/')
357359
self.assertEqual(res.url, 'http://example.com/')
360+
# Assert that we can redirect using UTF-8 strings
361+
res = redirect('/æøå/abc/')
362+
self.assertEqual(res.url, '/%C3%A6%C3%B8%C3%A5/abc/')
363+
# Assert that no imports are attempted when dealing with a relative path
364+
# (previously, the below would resolve in a UnicodeEncodeError from __import__ )
365+
res = redirect('/æøå.abc/')
366+
self.assertEqual(res.url, '/%C3%A6%C3%B8%C3%A5.abc/')
367+
res = redirect('os.path')
368+
self.assertEqual(res.url, 'os.path')
369+
370+
def test_no_illegal_imports(self):
371+
# modules that are not listed in urlpatterns should not be importable
372+
redirect("urlpatterns_reverse.nonimported_module.view")
373+
self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules)
374+
375+
def test_reverse_by_path_nested(self):
376+
# Views that are added to urlpatterns using include() should be
377+
# reversable by doted path.
378+
self.assertEqual(reverse('urlpatterns_reverse.views.nested_view'), '/includes/nested_path/')
358379

359380
def test_redirect_view_object(self):
360381
from .views import absolute_kwargs_view

tests/urlpatterns_reverse/urls.py

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

66
other_patterns = patterns('',
77
url(r'non_path_include/$', empty_view, name='non_path_include'),
8+
url(r'nested_path/$', 'urlpatterns_reverse.views.nested_view'),
89
)
910

1011
urlpatterns = patterns('',

tests/urlpatterns_reverse/views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ def defaults_view(request, arg1, arg2):
2121
pass
2222

2323

24+
def nested_view(request):
25+
pass
26+
27+
2428
def erroneous_view(request):
2529
import non_existent # NOQA
2630

0 commit comments

Comments
 (0)