Version in base suite: 3.9.0-1 Base version: djangorestframework_3.9.0-1 Target version: djangorestframework_3.9.0-1+deb10u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/d/djangorestframework/djangorestframework_3.9.0-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/d/djangorestframework/djangorestframework_3.9.0-1+deb10u1.dsc changelog | 10 + patches/CVE-2020-25626.patch | 214 +++++++++++++++++++++++++ patches/series | 2 patches/xss-in-default-drf-browsable-api.patch | 100 +++++++++++ 4 files changed, 326 insertions(+) diff -Nru djangorestframework-3.9.0/debian/changelog djangorestframework-3.9.0/debian/changelog --- djangorestframework-3.9.0/debian/changelog 2018-12-25 00:20:40.000000000 +0000 +++ djangorestframework-3.9.0/debian/changelog 2022-07-21 12:24:42.000000000 +0000 @@ -1,3 +1,13 @@ +djangorestframework (3.9.0-1+deb10u1) buster-security; urgency=medium + + * Backport 4bb9a3c48427867ef1e46f7dee945a4c25a4f9b8 to fix cross-site + scripting caused by disabled autoescaping in the default DRF + Browsable API view templates (no CVE yet) + * Backport ae649336b110afe21b9429f2554052f31a9dfaf9 to address + CVE-2020-25626 + + -- Moritz Mühlenhoff Thu, 21 Jul 2022 14:24:42 +0200 + djangorestframework (3.9.0-1) unstable; urgency=medium * Team upload diff -Nru djangorestframework-3.9.0/debian/patches/CVE-2020-25626.patch djangorestframework-3.9.0/debian/patches/CVE-2020-25626.patch --- djangorestframework-3.9.0/debian/patches/CVE-2020-25626.patch 1970-01-01 00:00:00.000000000 +0000 +++ djangorestframework-3.9.0/debian/patches/CVE-2020-25626.patch 2022-07-21 12:24:26.000000000 +0000 @@ -0,0 +1,214 @@ +Backport of + +From ae649336b110afe21b9429f2554052f31a9dfaf9 Mon Sep 17 00:00:00 2001 +From: Tom Christie +Date: Wed, 23 Sep 2020 15:39:06 +0100 +Subject: [PATCH] Drop urlize_quoted_links (#7548) + +--- djangorestframework-3.9.0.orig/rest_framework/renderers.py ++++ djangorestframework-3.9.0/rest_framework/renderers.py +@@ -428,7 +428,7 @@ class BrowsableAPIRenderer(BaseRenderer) + if render_style == 'binary': + return '[%d bytes of binary content]' % len(content) + +- return content ++ return content.decode('utf-8') if isinstance(content, bytes) else content + + def show_form_for_method(self, view, method, request, obj): + """ +--- djangorestframework-3.9.0.orig/rest_framework/templates/rest_framework/base.html ++++ djangorestframework-3.9.0/rest_framework/templates/rest_framework/base.html +@@ -172,9 +172,9 @@ + +
+
HTTP {{ response.status_code }} {{ response.status_text }}{% for key, val in response_headers|items %}
+-{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
++{{ key }}: {{ val|break_long_headers|urlize }}{% endfor %}
+ 
+-{{ content|urlize_quoted_links }}
++{{ content|urlize }} +
+ + +--- djangorestframework-3.9.0.orig/rest_framework/templatetags/rest_framework.py ++++ djangorestframework-3.9.0/rest_framework/templatetags/rest_framework.py +@@ -314,85 +314,6 @@ def smart_urlquote_wrapper(matched_url): + return None + + +-@register.filter(needs_autoescape=True) +-def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True): +- """ +- Converts any URLs in text into clickable links. +- +- Works on http://, https://, www. links, and also on links ending in one of +- the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org). +- Links can have trailing punctuation (periods, commas, close-parens) and +- leading punctuation (opening parens) and it'll still do the right thing. +- +- If trim_url_limit is not None, the URLs in link text longer than this limit +- will truncated to trim_url_limit-3 characters and appended with an ellipsis. +- +- If nofollow is True, the URLs in link text will get a rel="nofollow" +- attribute. +- +- If autoescape is True, the link text and URLs will get autoescaped. +- """ +- 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: +- # Deal with punctuation. +- lead, middle, trail = '', word, '' +- for punctuation in TRAILING_PUNCTUATION: +- if middle.endswith(punctuation): +- middle = middle[:-len(punctuation)] +- trail = punctuation + trail +- for opening, closing in WRAPPING_PUNCTUATION: +- if middle.startswith(opening): +- middle = middle[len(opening):] +- lead = lead + opening +- # Keep parentheses at the end only if they're balanced. +- if ( +- middle.endswith(closing) and +- middle.count(closing) == middle.count(opening) + 1 +- ): +- middle = middle[:-len(closing)] +- trail = closing + trail +- +- # Make URL we want to point to. +- url = None +- nofollow_attr = ' rel="nofollow"' if nofollow else '' +- if simple_url_re.match(middle): +- url = smart_urlquote_wrapper(middle) +- elif simple_url_2_re.match(middle): +- url = smart_urlquote_wrapper('http://%s' % middle) +- elif ':' not in middle and simple_email_re.match(middle): +- local, domain = middle.rsplit('@', 1) +- try: +- domain = domain.encode('idna').decode('ascii') +- except UnicodeError: +- continue +- url = 'mailto:%s@%s' % (local, domain) +- nofollow_attr = '' +- +- # Make link. +- if url: +- trimmed = trim_url(middle) +- lead, trail = conditional_escape(lead), conditional_escape(trail) +- url, trimmed = conditional_escape(url), conditional_escape(trimmed) +- middle = '%s' % (url, nofollow_attr, trimmed) +- words[i] = '%s%s%s' % (lead, middle, trail) +- else: +- words[i] = conditional_escape(word) +- else: +- words[i] = conditional_escape(word) +- return mark_safe(''.join(words)) +- +- + @register.filter + def break_long_headers(header): + """ +--- djangorestframework-3.9.0.orig/tests/test_templatetags.py ++++ djangorestframework-3.9.0/tests/test_templatetags.py +@@ -5,13 +5,14 @@ import unittest + + from django.template import Context, Template + from django.test import TestCase ++from django.utils.html import urlize + + from rest_framework.compat import coreapi, coreschema + from rest_framework.relations import Hyperlink + from rest_framework.templatetags import rest_framework + from rest_framework.templatetags.rest_framework import ( + add_nested_class, add_query_param, as_string, break_long_headers, +- format_value, get_pagination_html, schema_links, urlize_quoted_links ++ format_value, get_pagination_html, schema_links + ) + from rest_framework.test import APIRequestFactory + +@@ -249,7 +250,7 @@ class Issue1386Tests(TestCase): + + def test_issue_1386(self): + """ +- Test function urlize_quoted_links with different args ++ Test function urlize with different args + """ + correct_urls = [ + "asdf.com", +@@ -258,7 +259,7 @@ class Issue1386Tests(TestCase): + "as.d8f.ghj8.gov", + ] + for i in correct_urls: +- res = urlize_quoted_links(i) ++ res = urlize(i) + self.assertNotEqual(res, i) + self.assertIn(i, res) + +@@ -267,11 +268,11 @@ class Issue1386Tests(TestCase): + "asdf.netnet", + ] + for i in incorrect_urls: +- res = urlize_quoted_links(i) ++ res = urlize(i) + self.assertEqual(i, res) + + # example from issue #1386, this shouldn't raise an exception +- urlize_quoted_links("asdf:[/p]zxcv.com") ++ urlize("asdf:[/p]zxcv.com") + + def test_smart_urlquote_wrapper_handles_value_error(self): + def mock_smart_urlquote(url): +@@ -292,7 +293,10 @@ class URLizerTests(TestCase): + For all items in dict test assert that the value is urlized key + """ + for original, urlized in data.items(): +- assert urlize_quoted_links(original, nofollow=False) == urlized ++ print('====') ++ print(repr(urlize(original, nofollow=False))) ++ print(repr(urlized)) ++ assert urlize(original, nofollow=False) == urlized + + def test_json_with_url(self): + """ +@@ -300,26 +304,26 @@ class URLizerTests(TestCase): + """ + data = {} + data['"url": "http://api/users/1/", '] = \ +- '"url": "http://api/users/1/", ' ++ '"url": "http://api/users/1/", ' + data['"foo_set": [\n "http://api/foos/1/"\n], '] = \ +- '"foo_set": [\n "http://api/foos/1/"\n], ' ++ '"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 }}") ++ template = Template("{% load rest_framework %}{{ content|urlize }}") + 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. ++ Test if the autoescape value is getting passed to urlize filter. + """ + template = Template("{% load rest_framework %}" +- "{% autoescape off %}{{ content|urlize_quoted_links }}" ++ "{% autoescape off %}{{ content|urlize }}" + "{% endautoescape %}") + rendered = template.render(Context({'content': ' "http://example.com" '})) + assert rendered == ' "http://example.com" ' diff -Nru djangorestframework-3.9.0/debian/patches/series djangorestframework-3.9.0/debian/patches/series --- djangorestframework-3.9.0/debian/patches/series 2018-12-24 14:24:34.000000000 +0000 +++ djangorestframework-3.9.0/debian/patches/series 2022-07-21 12:24:07.000000000 +0000 @@ -1,2 +1,4 @@ 0001-Disable-Postgresql-tests.patch 0002-Clean-all-privacy-breaches-in-the-package.patch +xss-in-default-drf-browsable-api.patch +CVE-2020-25626.patch diff -Nru djangorestframework-3.9.0/debian/patches/xss-in-default-drf-browsable-api.patch djangorestframework-3.9.0/debian/patches/xss-in-default-drf-browsable-api.patch --- djangorestframework-3.9.0/debian/patches/xss-in-default-drf-browsable-api.patch 1970-01-01 00:00:00.000000000 +0000 +++ djangorestframework-3.9.0/debian/patches/xss-in-default-drf-browsable-api.patch 2022-07-21 12:15:51.000000000 +0000 @@ -0,0 +1,100 @@ +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 + +--- djangorestframework-3.9.0.orig/rest_framework/templates/rest_framework/base.html ++++ djangorestframework-3.9.0/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 }}
+
+ + +--- djangorestframework-3.9.0.orig/rest_framework/templatetags/rest_framework.py ++++ djangorestframework-3.9.0/rest_framework/templatetags/rest_framework.py +@@ -336,6 +336,12 @@ def urlize_quoted_links(text, trim_url_l + 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_l + # 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 +--- djangorestframework-3.9.0.orig/tests/test_templatetags.py ++++ djangorestframework-3.9.0/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')