From 4bb9a3c48427867ef1e46f7dee945a4c25a4f9b8 Mon Sep 17 00:00:00 2001 From: "Yury V. Zaytsev" Date: Wed, 16 Jan 2019 13:36:25 +0100 Subject: [PATCH] Fix XSS caused by disabled autoescaping in the default DRF Browsable API view templates (#6330) * Add test that verifies that HTML is correctly escaped in Browsable API views * Fix `urlize_quoted_links` tag to avoid double escaping in autoescape mode * Fix XSS in default DRF Browsable API template by re-enabling autoescape --- .../templates/rest_framework/base.html | 4 +-- rest_framework/templatetags/rest_framework.py | 26 +++++++++---------- tests/test_templatetags.py | 13 ++++++++-- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 26395e1fdc..688fd23104 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -171,10 +171,10 @@

{{ name }}

-
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 3923389737..f48675d5eb 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -336,6 +336,12 @@ def trim_url(x, limit=trim_url_limit): 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 trim_url(x, limit=trim_url_limit): # 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 5d4f6a4e3d..45bfd4aeb7 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -305,6 +305,15 @@ def test_json_with_url(self): '"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 @@ def test_template_render_with_noautoescape(self): 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')