From e05f2a75695b5f5faa7682d4053db4776d4d6f93 Mon Sep 17 00:00:00 2001 From: varunkasyap Date: Sat, 15 Nov 2025 10:36:46 +0530 Subject: [PATCH] Fixed #36733 -- Escaped attributes in Stylesheet.__str__(). Thanks Mustafa Barakat for the report, Baptiste Mispelon for the triage, and Jake Howard for the review. --- django/utils/feedgenerator.py | 13 ++++++------ docs/releases/5.2.9.txt | 5 ++++- tests/syndication_tests/tests.py | 28 ++++++++++++------------- tests/utils_tests/test_feedgenerator.py | 16 +++++++++++++- 4 files changed, 40 insertions(+), 22 deletions(-) diff --git a/django/utils/feedgenerator.py b/django/utils/feedgenerator.py index a9eaffc205..b42417bbba 100644 --- a/django/utils/feedgenerator.py +++ b/django/utils/feedgenerator.py @@ -28,6 +28,7 @@ import mimetypes from io import StringIO from urllib.parse import urlparse +from django.forms.utils import flatatt from django.utils.encoding import iri_to_uri from django.utils.xmlutils import SimplerXMLGenerator @@ -95,12 +96,12 @@ class Stylesheet: return self._mimetype def __str__(self): - data = [f'href="{self.url}"'] - if self.mimetype is not None: - data.append(f'type="{self.mimetype}"') - if self.media is not None: - data.append(f'media="{self.media}"') - return " ".join(data) + attrs = { + "href": iri_to_uri(self._url), + "type": self.mimetype, + "media": self.media, + } + return flatatt(attrs).strip() def __repr__(self): return repr((self.url, self.mimetype, self.media)) diff --git a/docs/releases/5.2.9.txt b/docs/releases/5.2.9.txt index 5698bdc206..0d726de640 100644 --- a/docs/releases/5.2.9.txt +++ b/docs/releases/5.2.9.txt @@ -9,4 +9,7 @@ Django 5.2.9 fixes several bugs in 5.2.8. Bugfixes ======== -* ... +* Fixed a bug in Django 5.2 where + ``django.utils.feedgenerator.Stylesheet.__str__()`` did not escape + the ``url``, ``mimetype``, and ``media`` attributes, potentially leading + to invalid XML markup (:ticket:`36733`). diff --git a/tests/syndication_tests/tests.py b/tests/syndication_tests/tests.py index 17a408a686..739139db63 100644 --- a/tests/syndication_tests/tests.py +++ b/tests/syndication_tests/tests.py @@ -578,51 +578,51 @@ class SyndicationFeedTest(FeedTestCase): def test_stylesheets(self): testdata = [ # Plain strings. - ("/test.xsl", 'href="/test.xsl" type="text/xsl" media="screen"'), - ("/test.xslt", 'href="/test.xslt" type="text/xsl" media="screen"'), - ("/test.css", 'href="/test.css" type="text/css" media="screen"'), + ("/test.xsl", 'href="/test.xsl" media="screen" type="text/xsl"'), + ("/test.xslt", 'href="/test.xslt" media="screen" type="text/xsl"'), + ("/test.css", 'href="/test.css" media="screen" type="text/css"'), ("/test", 'href="/test" media="screen"'), ( "https://example.com/test.xsl", - 'href="https://example.com/test.xsl" type="text/xsl" media="screen"', + 'href="https://example.com/test.xsl" media="screen" type="text/xsl"', ), ( "https://example.com/test.css", - 'href="https://example.com/test.css" type="text/css" media="screen"', + 'href="https://example.com/test.css" media="screen" type="text/css"', ), ( "https://example.com/test", 'href="https://example.com/test" media="screen"', ), - ("/♥.xsl", 'href="/%E2%99%A5.xsl" type="text/xsl" media="screen"'), + ("/♥.xsl", 'href="/%E2%99%A5.xsl" media="screen" type="text/xsl"'), ( static("stylesheet.xsl"), - 'href="/static/stylesheet.xsl" type="text/xsl" media="screen"', + 'href="/static/stylesheet.xsl" media="screen" type="text/xsl"', ), ( static("stylesheet.css"), - 'href="/static/stylesheet.css" type="text/css" media="screen"', + 'href="/static/stylesheet.css" media="screen" type="text/css"', ), (static("stylesheet"), 'href="/static/stylesheet" media="screen"'), ( reverse("syndication-xsl-stylesheet"), - 'href="/syndication/stylesheet.xsl" type="text/xsl" media="screen"', + 'href="/syndication/stylesheet.xsl" media="screen" type="text/xsl"', ), ( reverse_lazy("syndication-xsl-stylesheet"), - 'href="/syndication/stylesheet.xsl" type="text/xsl" media="screen"', + 'href="/syndication/stylesheet.xsl" media="screen" type="text/xsl"', ), # Stylesheet objects. ( Stylesheet("/test.xsl"), - 'href="/test.xsl" type="text/xsl" media="screen"', + 'href="/test.xsl" media="screen" type="text/xsl"', ), (Stylesheet("/test.xsl", mimetype=None), 'href="/test.xsl" media="screen"'), (Stylesheet("/test.xsl", media=None), 'href="/test.xsl" type="text/xsl"'), (Stylesheet("/test.xsl", mimetype=None, media=None), 'href="/test.xsl"'), ( Stylesheet("/test.xsl", mimetype="text/xml"), - 'href="/test.xsl" type="text/xml" media="screen"', + 'href="/test.xsl" media="screen" type="text/xml"', ), ] for stylesheet, expected in testdata: @@ -642,12 +642,12 @@ class SyndicationFeedTest(FeedTestCase): self.assertEqual(doc.childNodes[0].nodeName, "xml-stylesheet") self.assertEqual( doc.childNodes[0].data, - 'href="/stylesheet1.xsl" type="text/xsl" media="screen"', + 'href="/stylesheet1.xsl" media="screen" type="text/xsl"', ) self.assertEqual(doc.childNodes[1].nodeName, "xml-stylesheet") self.assertEqual( doc.childNodes[1].data, - 'href="/stylesheet2.xsl" type="text/xsl" media="screen"', + 'href="/stylesheet2.xsl" media="screen" type="text/xsl"', ) def test_stylesheets_typeerror_if_str_or_stylesheet(self): diff --git a/tests/utils_tests/test_feedgenerator.py b/tests/utils_tests/test_feedgenerator.py index 28a1afc96e..65e1faae6c 100644 --- a/tests/utils_tests/test_feedgenerator.py +++ b/tests/utils_tests/test_feedgenerator.py @@ -156,6 +156,20 @@ class FeedgeneratorTests(SimpleTestCase): stylesheet = feedgenerator.Stylesheet(SimpleLazyObject(m)) m.assert_not_called() self.assertEqual( - str(stylesheet), 'href="test.css" type="text/css" media="screen"' + str(stylesheet), 'href="test.css" media="screen" type="text/css"' ) m.assert_called_once() + + def test_stylesheet_attribute_escaping(self): + style = feedgenerator.Stylesheet( + url='http://example.com/style.css?foo="bar"&baz=<>', + mimetype='text/css; charset="utf-8"', + media='screen and (max-width: "600px")', + ) + + self.assertEqual( + str(style), + 'href="http://example.com/style.css?foo=%22bar%22&baz=%3C%3E" ' + 'media="screen and (max-width: "600px")" ' + 'type="text/css; charset="utf-8""', + )