티스토리 수익 글 보기

티스토리 수익 글 보기

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

Commit c1a8c42

Browse files
committed
[1.4.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 ca3927d commit c1a8c42

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
@@ -230,6 +230,10 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name
230230
self._reverse_dict = {}
231231
self._namespace_dict = {}
232232
self._app_dict = {}
233+
# set of dotted paths to all functions and classes that are used in
234+
# urlpatterns
235+
self._callback_strs = set()
236+
self._populated = False
233237

234238
def __repr__(self):
235239
return smart_str(u'<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern))
@@ -240,6 +244,15 @@ def _populate(self):
240244
apps = {}
241245
language_code = get_language()
242246
for pattern in reversed(self.url_patterns):
247+
if hasattr(pattern, '_callback_str'):
248+
self._callback_strs.add(pattern._callback_str)
249+
elif hasattr(pattern, '_callback'):
250+
callback = pattern._callback
251+
if not hasattr(callback, '__name__'):
252+
lookup_str = callback.__module__ + "." + callback.__class__.__name__
253+
else:
254+
lookup_str = callback.__module__ + "." + callback.__name__
255+
self._callback_strs.add(lookup_str)
243256
p_pattern = pattern.regex.pattern
244257
if p_pattern.startswith('^'):
245258
p_pattern = p_pattern[1:]
@@ -260,6 +273,7 @@ def _populate(self):
260273
namespaces[namespace] = (p_pattern + prefix, sub_pattern)
261274
for app_name, namespace_list in pattern.app_dict.items():
262275
apps.setdefault(app_name, []).extend(namespace_list)
276+
self._callback_strs.update(pattern._callback_strs)
263277
else:
264278
bits = normalize(p_pattern)
265279
lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args))
@@ -268,6 +282,7 @@ def _populate(self):
268282
self._reverse_dict[language_code] = lookups
269283
self._namespace_dict[language_code] = namespaces
270284
self._app_dict[language_code] = apps
285+
self._populated = True
271286

272287
@property
273288
def reverse_dict(self):
@@ -356,8 +371,13 @@ def reverse(self, lookup_view, *args, **kwargs):
356371
def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs):
357372
if args and kwargs:
358373
raise ValueError("Don't mix *args and **kwargs in call to reverse()!")
374+
375+
if not self._populated:
376+
self._populate()
377+
359378
try:
360-
lookup_view = get_callable(lookup_view, True)
379+
if lookup_view in self._callback_strs:
380+
lookup_view = get_callable(lookup_view, True)
361381
except (ImportError, AttributeError), e:
362382
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
363383
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
56

7+
import sys
8+
69
from django.conf import settings
710
from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
811
from django.core.urlresolvers import (reverse, resolve, NoReverseMatch,
@@ -267,6 +270,25 @@ def test_redirect_to_url(self):
267270
self.assertEqual(res['Location'], '/foo/')
268271
res = redirect('http://example.com/')
269272
self.assertEqual(res['Location'], 'http://example.com/')
273+
# Assert that we can redirect using UTF-8 strings
274+
res = redirect('/æøå/abc/')
275+
self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5/abc/')
276+
# Assert that no imports are attempted when dealing with a relative path
277+
# (previously, the below would resolve in a UnicodeEncodeError from __import__ )
278+
res = redirect('/æøå.abc/')
279+
self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5.abc/')
280+
res = redirect('os.path')
281+
self.assertEqual(res['Location'], 'os.path')
282+
283+
def test_no_illegal_imports(self):
284+
# modules that are not listed in urlpatterns should not be importable
285+
redirect("urlpatterns_reverse.nonimported_module.view")
286+
self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules)
287+
288+
def test_reverse_by_path_nested(self):
289+
# Views that are added to urlpatterns using include() should be
290+
# reversable by doted path.
291+
self.assertEqual(reverse('regressiontests.urlpatterns_reverse.views.nested_view'), '/includes/nested_path/')
270292

271293
def test_redirect_view_object(self):
272294
from .views import absolute_kwargs_view
@@ -510,4 +532,3 @@ def test_erroneous_resolve(self):
510532
self.assertRaises(ViewDoesNotExist, self.client.get, '/missing_inner/')
511533
self.assertRaises(ViewDoesNotExist, self.client.get, '/missing_outer/')
512534
self.assertRaises(ViewDoesNotExist, self.client.get, '/uncallable/')
513-

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)