Critical severityNVD Advisory· Published Apr 23, 2014· Updated May 6, 2026
CVE-2014-0472
CVE-2014-0472
Description
The django.core.urlresolvers.reverse function in Django before 1.4.11, 1.5.x before 1.5.6, 1.6.x before 1.6.3, and 1.7.x before 1.7 beta 2 allows remote attackers to import and execute arbitrary Python modules by leveraging a view that constructs URLs using user input and a "dotted Python path."
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
DjangoPyPI | < 1.4.11 | 1.4.11 |
DjangoPyPI | >= 1.5, < 1.5.6 | 1.5.6 |
DjangoPyPI | >= 1.6, < 1.6.3 | 1.6.3 |
Affected products
28cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*+ 22 more
- cpe:2.3:a:djangoproject:django:*:*:*:*:*:*:*:*range: <=1.4.10
- cpe:2.3:a:djangoproject:django:1.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.3:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.7:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.8:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.4.9:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.6.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:alpha1:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:alpha2:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.7:beta1:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.1:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.2:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.3:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.4:*:*:*:*:*:*:*
- cpe:2.3:a:djangoproject:django:1.5.5:*:*:*:*:*:*:*
cpe:2.3:o:canonical:ubuntu_linux:10.04:-:lts:*:*:*:*:*+ 4 more
- cpe:2.3:o:canonical:ubuntu_linux:10.04:-:lts:*:*:*:*:*
- cpe:2.3:o:canonical:ubuntu_linux:12.04:-:lts:*:*:*:*:*
- cpe:2.3:o:canonical:ubuntu_linux:12.10:*:*:*:*:*:*:*
- cpe:2.3:o:canonical:ubuntu_linux:13.10:*:*:*:*:*:*:*
- cpe:2.3:o:canonical:ubuntu_linux:14.04:*:*:*:lts:*:*:*
Patches
3c1a8c420fe4b[1.4.x] Fixed a remote code execution vulnerabilty in URL reversing.
5 files changed · +51 −2
django/core/urlresolvers.py+21 −1 modified@@ -230,6 +230,10 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name self._reverse_dict = {} self._namespace_dict = {} self._app_dict = {} + # set of dotted paths to all functions and classes that are used in + # urlpatterns + self._callback_strs = set() + self._populated = False def __repr__(self): 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): apps = {} language_code = get_language() for pattern in reversed(self.url_patterns): + if hasattr(pattern, '_callback_str'): + self._callback_strs.add(pattern._callback_str) + elif hasattr(pattern, '_callback'): + callback = pattern._callback + if not hasattr(callback, '__name__'): + lookup_str = callback.__module__ + "." + callback.__class__.__name__ + else: + lookup_str = callback.__module__ + "." + callback.__name__ + self._callback_strs.add(lookup_str) p_pattern = pattern.regex.pattern if p_pattern.startswith('^'): p_pattern = p_pattern[1:] @@ -260,6 +273,7 @@ def _populate(self): namespaces[namespace] = (p_pattern + prefix, sub_pattern) for app_name, namespace_list in pattern.app_dict.items(): apps.setdefault(app_name, []).extend(namespace_list) + self._callback_strs.update(pattern._callback_strs) else: bits = normalize(p_pattern) lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args)) @@ -268,6 +282,7 @@ def _populate(self): self._reverse_dict[language_code] = lookups self._namespace_dict[language_code] = namespaces self._app_dict[language_code] = apps + self._populated = True @property def reverse_dict(self): @@ -356,8 +371,13 @@ def reverse(self, lookup_view, *args, **kwargs): def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs): if args and kwargs: raise ValueError("Don't mix *args and **kwargs in call to reverse()!") + + if not self._populated: + self._populate() + try: - lookup_view = get_callable(lookup_view, True) + if lookup_view in self._callback_strs: + lookup_view = get_callable(lookup_view, True) except (ImportError, AttributeError), e: raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) possibilities = self.reverse_dict.getlist(lookup_view)
tests/regressiontests/urlpatterns_reverse/nonimported_module.py+3 −0 added@@ -0,0 +1,3 @@ +def view(request): + """Stub view""" + pass
tests/regressiontests/urlpatterns_reverse/tests.py+22 −1 modified@@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- """ Unit tests for reverse URL lookups. """ from __future__ import absolute_import +import sys + from django.conf import settings from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist from django.core.urlresolvers import (reverse, resolve, NoReverseMatch, @@ -267,6 +270,25 @@ def test_redirect_to_url(self): self.assertEqual(res['Location'], '/foo/') res = redirect('http://example.com/') self.assertEqual(res['Location'], 'http://example.com/') + # Assert that we can redirect using UTF-8 strings + res = redirect('/æøå/abc/') + self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5/abc/') + # Assert that no imports are attempted when dealing with a relative path + # (previously, the below would resolve in a UnicodeEncodeError from __import__ ) + res = redirect('/æøå.abc/') + self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5.abc/') + res = redirect('os.path') + self.assertEqual(res['Location'], 'os.path') + + def test_no_illegal_imports(self): + # modules that are not listed in urlpatterns should not be importable + redirect("urlpatterns_reverse.nonimported_module.view") + self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules) + + def test_reverse_by_path_nested(self): + # Views that are added to urlpatterns using include() should be + # reversable by doted path. + self.assertEqual(reverse('regressiontests.urlpatterns_reverse.views.nested_view'), '/includes/nested_path/') def test_redirect_view_object(self): from .views import absolute_kwargs_view @@ -510,4 +532,3 @@ def test_erroneous_resolve(self): self.assertRaises(ViewDoesNotExist, self.client.get, '/missing_inner/') self.assertRaises(ViewDoesNotExist, self.client.get, '/missing_outer/') self.assertRaises(ViewDoesNotExist, self.client.get, '/uncallable/') -
tests/regressiontests/urlpatterns_reverse/urls.py+1 −0 modified@@ -7,6 +7,7 @@ other_patterns = patterns('', url(r'non_path_include/$', empty_view, name='non_path_include'), + url(r'nested_path/$', 'regressiontests.urlpatterns_reverse.views.nested_view'), ) urlpatterns = patterns('',
tests/regressiontests/urlpatterns_reverse/views.py+4 −0 modified@@ -16,6 +16,10 @@ def absolute_kwargs_view(request, arg1=1, arg2=2): def defaults_view(request, arg1, arg2): pass +def nested_view(request): + pass + + def erroneous_view(request): import non_existent
2a5bcb69f42b[1.5.x] Fixed a remote code execution vulnerabilty in URL reversing.
5 files changed · +51 −2
django/core/urlresolvers.py+21 −1 modified@@ -244,6 +244,10 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name self._reverse_dict = {} self._namespace_dict = {} self._app_dict = {} + # set of dotted paths to all functions and classes that are used in + # urlpatterns + self._callback_strs = set() + self._populated = False def __repr__(self): if isinstance(self.urlconf_name, list) and len(self.urlconf_name): @@ -261,6 +265,15 @@ def _populate(self): apps = {} language_code = get_language() for pattern in reversed(self.url_patterns): + if hasattr(pattern, '_callback_str'): + self._callback_strs.add(pattern._callback_str) + elif hasattr(pattern, '_callback'): + callback = pattern._callback + if not hasattr(callback, '__name__'): + lookup_str = callback.__module__ + "." + callback.__class__.__name__ + else: + lookup_str = callback.__module__ + "." + callback.__name__ + self._callback_strs.add(lookup_str) p_pattern = pattern.regex.pattern if p_pattern.startswith('^'): p_pattern = p_pattern[1:] @@ -281,6 +294,7 @@ def _populate(self): namespaces[namespace] = (p_pattern + prefix, sub_pattern) for app_name, namespace_list in pattern.app_dict.items(): apps.setdefault(app_name, []).extend(namespace_list) + self._callback_strs.update(pattern._callback_strs) else: bits = normalize(p_pattern) lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args)) @@ -289,6 +303,7 @@ def _populate(self): self._reverse_dict[language_code] = lookups self._namespace_dict[language_code] = namespaces self._app_dict[language_code] = apps + self._populated = True @property def reverse_dict(self): @@ -375,8 +390,13 @@ def reverse(self, lookup_view, *args, **kwargs): def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs): if args and kwargs: raise ValueError("Don't mix *args and **kwargs in call to reverse()!") + + if not self._populated: + self._populate() + try: - lookup_view = get_callable(lookup_view, True) + if lookup_view in self._callback_strs: + lookup_view = get_callable(lookup_view, True) except (ImportError, AttributeError) as e: raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) possibilities = self.reverse_dict.getlist(lookup_view)
tests/regressiontests/urlpatterns_reverse/nonimported_module.py+3 −0 added@@ -0,0 +1,3 @@ +def view(request): + """Stub view""" + pass
tests/regressiontests/urlpatterns_reverse/tests.py+22 −1 modified@@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- """ Unit tests for reverse URL lookups. """ from __future__ import absolute_import, unicode_literals +import sys + from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist @@ -290,6 +293,25 @@ def test_redirect_to_url(self): self.assertEqual(res['Location'], '/foo/') res = redirect('http://example.com/') self.assertEqual(res['Location'], 'http://example.com/') + # Assert that we can redirect using UTF-8 strings + res = redirect('/æøå/abc/') + self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5/abc/') + # Assert that no imports are attempted when dealing with a relative path + # (previously, the below would resolve in a UnicodeEncodeError from __import__ ) + res = redirect('/æøå.abc/') + self.assertEqual(res['Location'], '/%C3%A6%C3%B8%C3%A5.abc/') + res = redirect('os.path') + self.assertEqual(res['Location'], 'os.path') + + def test_no_illegal_imports(self): + # modules that are not listed in urlpatterns should not be importable + redirect("urlpatterns_reverse.nonimported_module.view") + self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules) + + def test_reverse_by_path_nested(self): + # Views that are added to urlpatterns using include() should be + # reversable by doted path. + self.assertEqual(reverse('regressiontests.urlpatterns_reverse.views.nested_view'), '/includes/nested_path/') def test_redirect_view_object(self): from .views import absolute_kwargs_view @@ -559,4 +581,3 @@ def test_view_loading(self): # swallow it. self.assertRaises(AttributeError, get_callable, 'regressiontests.urlpatterns_reverse.views_broken.i_am_broken') -
tests/regressiontests/urlpatterns_reverse/urls.py+1 −0 modified@@ -7,6 +7,7 @@ other_patterns = patterns('', url(r'non_path_include/$', empty_view, name='non_path_include'), + url(r'nested_path/$', 'regressiontests.urlpatterns_reverse.views.nested_view'), ) urlpatterns = patterns('',
tests/regressiontests/urlpatterns_reverse/views.py+4 −0 modified@@ -16,6 +16,10 @@ def absolute_kwargs_view(request, arg1=1, arg2=2): def defaults_view(request, arg1, arg2): pass +def nested_view(request): + pass + + def erroneous_view(request): import non_existent
4352a50871e2[1.6.x] Fixed a remote code execution vulnerabilty in URL reversing.
5 files changed · +50 −2
django/core/urlresolvers.py+20 −1 modified@@ -243,6 +243,10 @@ def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, name self._reverse_dict = {} self._namespace_dict = {} self._app_dict = {} + # set of dotted paths to all functions and classes that are used in + # urlpatterns + self._callback_strs = set() + self._populated = False def __repr__(self): if isinstance(self.urlconf_name, list) and len(self.urlconf_name): @@ -260,6 +264,15 @@ def _populate(self): apps = {} language_code = get_language() for pattern in reversed(self.url_patterns): + if hasattr(pattern, '_callback_str'): + self._callback_strs.add(pattern._callback_str) + elif hasattr(pattern, '_callback'): + callback = pattern._callback + if not hasattr(callback, '__name__'): + lookup_str = callback.__module__ + "." + callback.__class__.__name__ + else: + lookup_str = callback.__module__ + "." + callback.__name__ + self._callback_strs.add(lookup_str) p_pattern = pattern.regex.pattern if p_pattern.startswith('^'): p_pattern = p_pattern[1:] @@ -280,6 +293,7 @@ def _populate(self): namespaces[namespace] = (p_pattern + prefix, sub_pattern) for app_name, namespace_list in pattern.app_dict.items(): apps.setdefault(app_name, []).extend(namespace_list) + self._callback_strs.update(pattern._callback_strs) else: bits = normalize(p_pattern) lookups.appendlist(pattern.callback, (bits, p_pattern, pattern.default_args)) @@ -288,6 +302,7 @@ def _populate(self): self._reverse_dict[language_code] = lookups self._namespace_dict[language_code] = namespaces self._app_dict[language_code] = apps + self._populated = True @property def reverse_dict(self): @@ -380,8 +395,12 @@ def _reverse_with_prefix(self, lookup_view, _prefix, *args, **kwargs): text_args = [force_text(v) for v in args] text_kwargs = dict((k, force_text(v)) for (k, v) in kwargs.items()) + if not self._populated: + self._populate() + try: - lookup_view = get_callable(lookup_view, True) + if lookup_view in self._callback_strs: + lookup_view = get_callable(lookup_view, True) except (ImportError, AttributeError) as e: raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) possibilities = self.reverse_dict.getlist(lookup_view)
tests/urlpatterns_reverse/nonimported_module.py+3 −0 added@@ -0,0 +1,3 @@ +def view(request): + """Stub view""" + pass
tests/urlpatterns_reverse/tests.py+22 −1 modified@@ -1,8 +1,11 @@ +# -*- coding: utf-8 -*- """ Unit tests for reverse URL lookups. """ from __future__ import absolute_import, unicode_literals +import sys + from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist @@ -313,6 +316,25 @@ def test_redirect_to_url(self): self.assertEqual(res.url, '/foo/') res = redirect('http://example.com/') self.assertEqual(res.url, 'http://example.com/') + # Assert that we can redirect using UTF-8 strings + res = redirect('/æøå/abc/') + self.assertEqual(res.url, '/%C3%A6%C3%B8%C3%A5/abc/') + # Assert that no imports are attempted when dealing with a relative path + # (previously, the below would resolve in a UnicodeEncodeError from __import__ ) + res = redirect('/æøå.abc/') + self.assertEqual(res.url, '/%C3%A6%C3%B8%C3%A5.abc/') + res = redirect('os.path') + self.assertEqual(res.url, 'os.path') + + def test_no_illegal_imports(self): + # modules that are not listed in urlpatterns should not be importable + redirect("urlpatterns_reverse.nonimported_module.view") + self.assertNotIn("urlpatterns_reverse.nonimported_module", sys.modules) + + def test_reverse_by_path_nested(self): + # Views that are added to urlpatterns using include() should be + # reversable by doted path. + self.assertEqual(reverse('urlpatterns_reverse.views.nested_view'), '/includes/nested_path/') def test_redirect_view_object(self): from .views import absolute_kwargs_view @@ -641,4 +663,3 @@ def test_view_loading(self): # swallow it. self.assertRaises(AttributeError, get_callable, 'urlpatterns_reverse.views_broken.i_am_broken') -
tests/urlpatterns_reverse/urls.py+1 −0 modified@@ -7,6 +7,7 @@ other_patterns = patterns('', url(r'non_path_include/$', empty_view, name='non_path_include'), + url(r'nested_path/$', 'urlpatterns_reverse.views.nested_view'), ) urlpatterns = patterns('',
tests/urlpatterns_reverse/views.py+4 −0 modified@@ -16,6 +16,10 @@ def absolute_kwargs_view(request, arg1=1, arg2=2): def defaults_view(request, arg1, arg2): pass +def nested_view(request): + pass + + def erroneous_view(request): import non_existent
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- github.com/advisories/GHSA-rvq6-mrpv-m6rmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-0472ghsaADVISORY
- www.djangoproject.com/weblog/2014/apr/21/security/nvdVendor Advisory
- lists.opensuse.org/opensuse-updates/2014-09/msg00023.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2014-0456.htmlnvdWEB
- rhn.redhat.com/errata/RHSA-2014-0457.htmlnvdWEB
- www.debian.org/security/2014/dsa-2934nvdWEB
- www.ubuntu.com/usn/USN-2169-1nvdWEB
- github.com/django/django/commit/2a5bcb69f42b84464b24b5c835dca6467b6aa7f1ghsaWEB
- github.com/django/django/commit/4352a50871e239ebcdf64eee6f0b88e714015c1bghsaWEB
- github.com/django/django/commit/c1a8c420fe4b27fb2caf5e46d23b5712fc0ac535ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/django/PYSEC-2014-1.yamlghsaWEB
- www.djangoproject.com/weblog/2014/apr/21/securityghsaWEB
- secunia.com/advisories/61281nvd
News mentions
0No linked articles in our index yet.