diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 26395e1fd..688fd2310 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -171,10 +171,10 @@
-
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}{% for key, val in response_headers|items %}
+                
HTTP {{ response.status_code }} {{ response.status_text }}{% for key, val in response_headers|items %}
 {{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
 
-{{ content|urlize_quoted_links }}
{% endautoescape %} +
{{ content|urlize_quoted_links }}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 392338973..f48675d5e 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -336,6 +336,12 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x safe_input = isinstance(text, SafeData) + + # Unfortunately, Django built-in cannot be used here, because escaping + # is to be performed on words, which have been forcibly coerced to text + def conditional_escape(text): + return escape(text) if autoescape and not safe_input else text + words = word_split_re.split(force_text(text)) for i, word in enumerate(words): if '.' in word or '@' in word or ':' in word: @@ -376,21 +382,15 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru # Make link. if url: trimmed = trim_url(middle) - if autoescape and not safe_input: - lead, trail = escape(lead), escape(trail) - url, trimmed = escape(url), escape(trimmed) + lead, trail = conditional_escape(lead), conditional_escape(trail) + url, trimmed = conditional_escape(url), conditional_escape(trimmed) middle = '%s' % (url, nofollow_attr, trimmed) - words[i] = mark_safe('%s%s%s' % (lead, middle, trail)) + words[i] = '%s%s%s' % (lead, middle, trail) else: - if safe_input: - words[i] = mark_safe(word) - elif autoescape: - words[i] = escape(word) - elif safe_input: - words[i] = mark_safe(word) - elif autoescape: - words[i] = escape(word) - return ''.join(words) + words[i] = conditional_escape(word) + else: + words[i] = conditional_escape(word) + return mark_safe(''.join(words)) @register.filter diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 5d4f6a4e3..45bfd4aeb 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -305,6 +305,15 @@ class URLizerTests(TestCase): '"foo_set": [\n "http://api/foos/1/"\n], ' self._urlize_dict_check(data) + def test_template_render_with_autoescape(self): + """ + Test that HTML is correctly escaped in Browsable API views. + """ + template = Template("{% load rest_framework %}{{ content|urlize_quoted_links }}") + rendered = template.render(Context({'content': ' http://example.com'})) + assert rendered == '<script>alert()</script>' \ + ' http://example.com' + def test_template_render_with_noautoescape(self): """ Test if the autoescape value is getting passed to urlize_quoted_links filter. @@ -312,8 +321,8 @@ class URLizerTests(TestCase): template = Template("{% load rest_framework %}" "{% autoescape off %}{{ content|urlize_quoted_links }}" "{% endautoescape %}") - rendered = template.render(Context({'content': '"http://example.com"'})) - assert rendered == '"http://example.com"' + rendered = template.render(Context({'content': ' "http://example.com" '})) + assert rendered == ' "http://example.com" ' @unittest.skipUnless(coreapi, 'coreapi is not installed')