diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 6d75cb7cae..006bb1949d 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -406,6 +406,8 @@ class RegexURLResolver(LocaleRegexProvider): unicode_kwargs = dict([(k, force_unicode(v)) for (k, v) in kwargs.items()]) candidate = (prefix_norm + result) % unicode_kwargs if re.search(u'^%s%s' % (_prefix, pattern), candidate, re.UNICODE): + if candidate.startswith('//'): + candidate = '/%%2F%s' % candidate[2:] return candidate # lookup_view can be URL label, or dotted path, or callable, Any of # these can be passed in at the top, but callables are not friendly in diff --git a/docs/releases/1.4.14.txt b/docs/releases/1.4.14.txt index d0032e5399..28390c96a4 100644 --- a/docs/releases/1.4.14.txt +++ b/docs/releases/1.4.14.txt @@ -5,3 +5,16 @@ Django 1.4.14 release notes *Under development* Django 1.4.14 fixes several security issues in 1.4.13. + +:func:`~django.core.urlresolvers.reverse()` could generate URLs pointing to other hosts +======================================================================================= + +In certain situations, URL reversing could generate scheme-relative URLs (URLs +starting with two slashes), which could unexpectedly redirect a user to a +different host. An attacker could exploit this, for example, by redirecting +users to a phishing site designed to ask for user's passwords. + +To remedy this, URL reversing now ensures that no URL starts with two slashes +(//), replacing the second slash with its URL encoded counterpart (%2F). This +approach ensures that semantics stay the same, while making the URL relative to +the domain and not to the scheme. diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py index 0ea732b8ab..6b4a06f3bb 100644 --- a/tests/regressiontests/urlpatterns_reverse/tests.py +++ b/tests/regressiontests/urlpatterns_reverse/tests.py @@ -142,6 +142,9 @@ test_data = ( ('defaults', '/defaults_view2/3/', [], {'arg1': 3, 'arg2': 2}), ('defaults', NoReverseMatch, [], {'arg1': 3, 'arg2': 3}), ('defaults', NoReverseMatch, [], {'arg2': 1}), + + # Security tests + ('security', '/%2Fexample.com/security/', ['/example.com'], {}), ) class NoURLPatternsTests(TestCase): diff --git a/tests/regressiontests/urlpatterns_reverse/urls.py b/tests/regressiontests/urlpatterns_reverse/urls.py index 7aae7c4691..0d3f8c3ed5 100644 --- a/tests/regressiontests/urlpatterns_reverse/urls.py +++ b/tests/regressiontests/urlpatterns_reverse/urls.py @@ -71,4 +71,7 @@ urlpatterns = patterns('', (r'defaults_view2/(?P\d+)/', 'defaults_view', {'arg2': 2}, 'defaults'), url('^includes/', include(other_patterns)), + + # Security tests + url('(.+)/security/$', empty_view, name='security'), )