Fixed #18210 -- Escaped special characters in reverse prefixes.

Ensured that special characters passed in to reverse via the
prefix argument are properly escaped so that calls to
django.utils.regex_helpers.normalize and/or string formatting
operations don't result in exceptions.

Thanks to toofishes for the error report.
This commit is contained in:
Gabriel Hurley 2012-11-03 13:06:57 -07:00
parent 0546794397
commit 90e530978d
2 changed files with 18 additions and 4 deletions

View File

@ -16,6 +16,7 @@ from django.core.exceptions import ImproperlyConfigured, ViewDoesNotExist
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.encoding import force_str, force_text, iri_to_uri from django.utils.encoding import force_str, force_text, iri_to_uri
from django.utils.functional import memoize, lazy from django.utils.functional import memoize, lazy
from django.utils.http import urlquote
from django.utils.importlib import import_module from django.utils.importlib import import_module
from django.utils.module_loading import module_has_submodule from django.utils.module_loading import module_has_submodule
from django.utils.regex_helper import normalize from django.utils.regex_helper import normalize
@ -379,14 +380,15 @@ class RegexURLResolver(LocaleRegexProvider):
except (ImportError, AttributeError) as e: except (ImportError, AttributeError) as e:
raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e)) raise NoReverseMatch("Error importing '%s': %s." % (lookup_view, e))
possibilities = self.reverse_dict.getlist(lookup_view) possibilities = self.reverse_dict.getlist(lookup_view)
prefix_norm, prefix_args = normalize(_prefix)[0]
prefix_norm, prefix_args = normalize(urlquote(_prefix))[0]
for possibility, pattern, defaults in possibilities: for possibility, pattern, defaults in possibilities:
for result, params in possibility: for result, params in possibility:
if args: if args:
if len(args) != len(params) + len(prefix_args): if len(args) != len(params) + len(prefix_args):
continue continue
unicode_args = [force_text(val) for val in args] unicode_args = [force_text(val) for val in args]
candidate = (prefix_norm + result) % dict(zip(prefix_args + params, unicode_args)) candidate = (prefix_norm + result) % dict(zip(prefix_args + params, unicode_args))
else: else:
if set(kwargs.keys()) | set(defaults.keys()) != set(params) | set(defaults.keys()) | set(prefix_args): if set(kwargs.keys()) | set(defaults.keys()) != set(params) | set(defaults.keys()) | set(prefix_args):
continue continue
@ -398,8 +400,8 @@ class RegexURLResolver(LocaleRegexProvider):
if not matches: if not matches:
continue continue
unicode_kwargs = dict([(k, force_text(v)) for (k, v) in kwargs.items()]) unicode_kwargs = dict([(k, force_text(v)) for (k, v) in kwargs.items()])
candidate = (prefix_norm + result) % unicode_kwargs candidate = (prefix_norm.replace('%', '%%') + result) % unicode_kwargs
if re.search('^%s%s' % (_prefix, pattern), candidate, re.UNICODE): if re.search('^%s%s' % (prefix_norm, pattern), candidate, re.UNICODE):
return candidate return candidate
# lookup_view can be URL label, or dotted path, or callable, Any of # 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 # these can be passed in at the top, but callables are not friendly in

View File

@ -171,6 +171,18 @@ class URLPatternReverse(TestCase):
# Reversing None should raise an error, not return the last un-named view. # Reversing None should raise an error, not return the last un-named view.
self.assertRaises(NoReverseMatch, reverse, None) self.assertRaises(NoReverseMatch, reverse, None)
def test_prefix_braces(self):
self.assertEqual('/%7B%7Binvalid%7D%7D/includes/non_path_include/',
reverse('non_path_include', prefix='/{{invalid}}/'))
def test_prefix_parenthesis(self):
self.assertEqual('/bogus%29/includes/non_path_include/',
reverse('non_path_include', prefix='/bogus)/'))
def test_prefix_format_char(self):
self.assertEqual('/bump%2520map/includes/non_path_include/',
reverse('non_path_include', prefix='/bump%20map/'))
class ResolverTests(unittest.TestCase): class ResolverTests(unittest.TestCase):
def test_resolver_repr(self): def test_resolver_repr(self):
""" """