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')