CVE-2018-25045 (Python, published 2022-07-23T02:15Z)
Description: value: Django REST framework (aka django-rest-framework) before 3.9.1 allows XSS because the default DRF Browsable API view templates disable autoescaping.
Severity: nan (CVSS v3 base score: 6.1)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/encode/django-rest-framework
Files changed:
rest_framework/templates/rest_framework/base.html
rest_framework/templatetags/rest_framework.py
tests/test_templatetags.py
Commit message: 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


Vulnerable code:
                <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}{% for key, val in response_headers|items %}
</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
                if autoescape and not safe_input:
                    lead, trail = escape(lead), escape(trail)
                    url, trimmed = escape(url), escape(trimmed)
                words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
                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)
        rendered = template.render(Context({'content': '"http://example.com"'}))
        assert rendered == '"<a href="http://example.com" rel="nofollow">http://example.com</a>"'

Fixed code:
                <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% for key, val in response_headers|items %}
</span>{{ content|urlize_quoted_links }}</pre>

    # 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

                lead, trail = conditional_escape(lead), conditional_escape(trail)
                url, trimmed = conditional_escape(url), conditional_escape(trimmed)
                words[i] = '%s%s%s' % (lead, middle, trail)
                words[i] = conditional_escape(word)
        else:
            words[i] = conditional_escape(word)
    return mark_safe(''.join(words))
    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': '<script>alert()</script> http://example.com'}))
        assert rendered == '&lt;script&gt;alert()&lt;/script&gt;' \
                           ' <a href="http://example.com" rel="nofollow">http://example.com</a>'

        rendered = template.render(Context({'content': '<b> "http://example.com" </b>'}))
        assert rendered == '<b> "<a href="http://example.com" rel="nofollow">http://example.com</a>" </b>'