From patchwork Wed Oct 22 06:17:55 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Ankur Tyagi X-Patchwork-Id: 72836 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from aws-us-west-2-korg-lkml-1.web.codeaurora.org (localhost.localdomain [127.0.0.1]) by smtp.lore.kernel.org (Postfix) with ESMTP id F0CD8CCD1AB for ; Wed, 22 Oct 2025 06:18:52 +0000 (UTC) Received: from mail-pg1-f182.google.com (mail-pg1-f182.google.com [209.85.215.182]) by mx.groups.io with SMTP id smtpd.web11.2523.1761113915307683992 for ; Tue, 21 Oct 2025 23:18:35 -0700 Authentication-Results: mx.groups.io; dkim=pass header.i=@gmail.com header.s=20230601 header.b=MyZzkmC1; spf=pass (domain: gmail.com, ip: 209.85.215.182, mailfrom: ankur.tyagi85@gmail.com) Received: by mail-pg1-f182.google.com with SMTP id 41be03b00d2f7-b6a7d3040efso2722959a12.1 for ; Tue, 21 Oct 2025 23:18:35 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1761113914; x=1761718714; darn=lists.openembedded.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=uyuSiPadbMHwnZD7Dp1d3dA6gkHzwvWmuUovr3lC8NA=; b=MyZzkmC1Erx4e95MjWwwS3w6yfRr8Yo0dY83kIY2UrcpnaQ9aJzholer8N4zoD7yC2 7WRMu/cFDcefh/kJrSny/t7oFquetq/Z4MB0xYZNrYT5M1G+R0MQVcxfuj9dt8Y4tWHm oiSW9tlJFZoLFms0BogNTtzQM8PAmECu8Xa++Yovi7kcqtg3iVPRv3v4RvuxMSDqhbUS 1qUM5OcVJ4e/CBFLVoDQYpWr0ZwFHXhF5KnuQLy+9bFTVlvuPAc+5zZaOiXQHJ5ZyEJV PaAi9vo3MQUM1TWHDWhsRWU+OinShmsC+WBC8/HD6+bix2KVRtm7WQLlYdqC3fvxdQC/ U5pw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1761113914; x=1761718714; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=uyuSiPadbMHwnZD7Dp1d3dA6gkHzwvWmuUovr3lC8NA=; b=AKk9g0bhTDXhNjlCK3QjavWdgL51pQ4HsMAfVBpC5szkJyx355EmAU/BjEjNeq8BKh 4kWcZFQ3kvEc0hak6Sso/OQwRsaiO2zO7nYHLkITZMPQNvfeIzfQXoIVldX+ThdZNlgk bGVl82ZUTEBL5swTACQ5umfQI0hM9Vw+mP5Ylsjhav2HpFAx24x5hBseEm7pYoTFVxI8 p/2yWjnmmbY4WAMgpLlcJu6ioQf6zAwfFSShreC/FUJpLm3E2kDsjI+cJ9JBJ3ksTYjH n8/FRj64mq4uR0bZZOXqldPoQmAFrtT1p8/HlLH6Pw3t/CnH5bBEtnyrqD0rqYrGNX1Y LTDg== X-Gm-Message-State: AOJu0YysTN6WE7SlJnqkEhzgXXfFY5DXDxI9E9R/DxPKnsyWIFia0imn d5HIas0aqZIASF4ugcdr3YySiR+V4lxIYy0dRZqYm7xeFKIECn/aog+KoG47YQ== X-Gm-Gg: ASbGncvl9ntmDHilqZFJindQwFaaNIGtv+Tfw+p3hEf+jfRFwtYjOSL7IDwsKYSQ5Q1 2rhc7mNbNRk2IRtHAFA8LvpZh5DaltsBNFT3EXgdpWGvgo6YNgOfWYZNQJpV+VfG96SxqMu4GHg 1BERUFHcZGvTL6jLlYA5adZYAmcsjk6z70Fxw60ClM65Y4TR9EjMOyxNQseYlYINnwf7f9epxBW WTTcVthCToNZEDRaFvTdDuMVBkZk1vw3k/6qsD9rbk5zrqB6lTrlng74rRYcTMNHLvSK0Bck8Nj ZczvvcmNqCEWe5DR7K7ICq3IAU5862toEgBuiHRUdn3acSD6rPnv+0YhVn74ycQCCACKDmDoZHa Tsz2472ZxFBpgFrD++vcS0lYhl/yad4B6FvdDK0YjGYgKWe+5F3Np2bVCC0BHnliIijk+Dxyj8E blwlncx6Z3vUXQZO1GzCuLF7ka X-Google-Smtp-Source: AGHT+IHH8M6USPQai+nXGcEaxtaKwdPw1ceNpCC31aZijmj/kuZhMeldNxiP7x1/mNEZKE1I5Pi2mw== X-Received: by 2002:a17:903:138a:b0:28e:c9f6:867b with SMTP id d9443c01a7336-290c9cd4adbmr233125285ad.23.1761113914121; Tue, 21 Oct 2025 23:18:34 -0700 (PDT) Received: from NVAPF55DW0D-IPD.. ([147.161.216.252]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-292471fde09sm127857485ad.93.2025.10.21.23.18.32 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 21 Oct 2025 23:18:33 -0700 (PDT) From: Ankur Tyagi To: openembedded-devel@lists.openembedded.org Cc: Ankur Tyagi Subject: [oe][meta-python][scarthgap][PATCH 4/8] python3-django: patch CVE-2025-48432 Date: Wed, 22 Oct 2025 19:17:55 +1300 Message-ID: <20251022061803.887676-4-ankur.tyagi85@gmail.com> X-Mailer: git-send-email 2.43.0 In-Reply-To: <20251022061803.887676-1-ankur.tyagi85@gmail.com> References: <20251022061803.887676-1-ankur.tyagi85@gmail.com> MIME-Version: 1.0 List-Id: X-Webhook-Received: from li982-79.members.linode.com [45.33.32.79] by aws-us-west-2-korg-lkml-1.web.codeaurora.org with HTTPS for ; Wed, 22 Oct 2025 06:18:52 -0000 X-Groupsio-URL: https://lists.openembedded.org/g/openembedded-devel/message/120887 https://nvd.nist.gov/vuln/detail/CVE-2025-48432 Following patches are needed to avoid cherry-pick conflicts - CVE-2025-48432-1.patch - CVE-2025-48432-2.patch - CVE-2025-48432-4.patch Signed-off-by: Ankur Tyagi --- .../CVE-2025-48432-1.patch | 166 +++++++++++++ .../CVE-2025-48432-2.patch | 225 ++++++++++++++++++ .../CVE-2025-48432-3.patch | 164 +++++++++++++ .../CVE-2025-48432-4.patch | 193 +++++++++++++++ .../CVE-2025-48432-5.patch | 76 ++++++ .../CVE-2025-48432-6.patch | 144 +++++++++++ .../python/python3-django_4.2.20.bb | 6 + 7 files changed, 974 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-1.patch create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-2.patch create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-3.patch create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-4.patch create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-5.patch create mode 100644 meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-6.patch diff --git a/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-1.patch b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-1.patch new file mode 100644 index 0000000000..51196d0ceb --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-1.patch @@ -0,0 +1,166 @@ +From d4a83d38022de809f4ae015ff4b7592c11f3b371 Mon Sep 17 00:00:00 2001 +From: Natalia <124304+nessita@users.noreply.github.com> +Date: Mon, 19 May 2025 22:45:38 -0300 +Subject: [PATCH] [4.2.x] Refs #26688 -- Added tests for `log_response()` + internal helper. + +Backport of 897046815944cc9a2da7ed9e8082f45ffe8110e3 from main. + +CVE: CVE-2025-48432 +Upstream-Status: Backport [https://github.com/django/django/commit/acbe655a0fa1200d2de31c6020f310ba9aa2f636] +(cherry picked from commit acbe655a0fa1200d2de31c6020f310ba9aa2f636) +Signed-off-by: Ankur Tyagi +--- + tests/logging_tests/tests.py | 121 +++++++++++++++++++++++++++++++++++ + 1 file changed, 121 insertions(+) + +diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py +index c73a3acd6d..2138a7fe50 100644 +--- a/tests/logging_tests/tests.py ++++ b/tests/logging_tests/tests.py +@@ -1,6 +1,7 @@ + import logging + from contextlib import contextmanager + from io import StringIO ++from unittest import TestCase + + from admin_scripts.tests import AdminScriptTestCase + +@@ -9,6 +10,7 @@ from django.core import mail + from django.core.exceptions import DisallowedHost, PermissionDenied, SuspiciousOperation + from django.core.files.temp import NamedTemporaryFile + from django.core.management import color ++from django.http import HttpResponse + from django.http.multipartparser import MultiPartParserError + from django.test import RequestFactory, SimpleTestCase, override_settings + from django.test.utils import LoggingCaptureMixin +@@ -19,6 +21,7 @@ from django.utils.log import ( + RequireDebugFalse, + RequireDebugTrue, + ServerFormatter, ++ log_response, + ) + from django.views.debug import ExceptionReporter + +@@ -646,3 +649,121 @@ class LogFormattersTests(SimpleTestCase): + self.assertRegex( + logger_output.getvalue(), r"^\[[/:,\w\s\d]+\] %s\n" % log_msg + ) ++ ++ ++class LogResponseRealLoggerTests(TestCase): ++ request = RequestFactory().get("/test-path/") ++ ++ def assertResponseLogged(self, logger_cm, msg, levelno, status_code, request): ++ self.assertEqual( ++ records_len := len(logger_cm.records), ++ 1, ++ f"Unexpected number of records for {logger_cm=} in {levelno=} (expected 1, " ++ f"got {records_len}).", ++ ) ++ record = logger_cm.records[0] ++ self.assertEqual(record.getMessage(), msg) ++ self.assertEqual(record.levelno, levelno) ++ self.assertEqual(record.status_code, status_code) ++ self.assertEqual(record.request, request) ++ ++ def test_missing_response_raises_attribute_error(self): ++ with self.assertRaises(AttributeError): ++ log_response("No response provided", response=None, request=self.request) ++ ++ def test_missing_request_logs_with_none(self): ++ response = HttpResponse(status=403) ++ with self.assertLogs("django.request", level="INFO") as cm: ++ log_response(msg := "Missing request", response=response, request=None) ++ self.assertResponseLogged(cm, msg, logging.WARNING, 403, request=None) ++ ++ def test_logs_5xx_as_error(self): ++ response = HttpResponse(status=508) ++ with self.assertLogs("django.request", level="ERROR") as cm: ++ log_response( ++ msg := "Server error occurred", response=response, request=self.request ++ ) ++ self.assertResponseLogged(cm, msg, logging.ERROR, 508, self.request) ++ ++ def test_logs_4xx_as_warning(self): ++ response = HttpResponse(status=418) ++ with self.assertLogs("django.request", level="WARNING") as cm: ++ log_response( ++ msg := "This is a teapot!", response=response, request=self.request ++ ) ++ self.assertResponseLogged(cm, msg, logging.WARNING, 418, self.request) ++ ++ def test_logs_2xx_as_info(self): ++ response = HttpResponse(status=201) ++ with self.assertLogs("django.request", level="INFO") as cm: ++ log_response(msg := "OK response", response=response, request=self.request) ++ self.assertResponseLogged(cm, msg, logging.INFO, 201, self.request) ++ ++ def test_custom_log_level(self): ++ response = HttpResponse(status=403) ++ with self.assertLogs("django.request", level="DEBUG") as cm: ++ log_response( ++ msg := "Debug level log", ++ response=response, ++ request=self.request, ++ level="debug", ++ ) ++ self.assertResponseLogged(cm, msg, logging.DEBUG, 403, self.request) ++ ++ def test_logs_only_once_per_response(self): ++ response = HttpResponse(status=500) ++ with self.assertLogs("django.request", level="ERROR") as cm: ++ log_response("First log", response=response, request=self.request) ++ log_response("Second log", response=response, request=self.request) ++ self.assertResponseLogged(cm, "First log", logging.ERROR, 500, self.request) ++ ++ def test_exc_info_output(self): ++ response = HttpResponse(status=500) ++ try: ++ raise ValueError("Simulated failure") ++ except ValueError as exc: ++ with self.assertLogs("django.request", level="ERROR") as cm: ++ log_response( ++ "With exception", ++ response=response, ++ request=self.request, ++ exception=exc, ++ ) ++ self.assertResponseLogged( ++ cm, "With exception", logging.ERROR, 500, self.request ++ ) ++ self.assertIn("ValueError", "\n".join(cm.output)) # Stack trace included ++ ++ def test_format_args_are_applied(self): ++ response = HttpResponse(status=500) ++ with self.assertLogs("django.request", level="ERROR") as cm: ++ log_response( ++ "Something went wrong: %s (%d)", ++ "DB error", ++ 42, ++ response=response, ++ request=self.request, ++ ) ++ msg = "Something went wrong: DB error (42)" ++ self.assertResponseLogged(cm, msg, logging.ERROR, 500, self.request) ++ ++ def test_logs_with_custom_logger(self): ++ handler = logging.StreamHandler(log_stream := StringIO()) ++ handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s")) ++ ++ custom_logger = logging.getLogger("my.custom.logger") ++ custom_logger.setLevel(logging.DEBUG) ++ custom_logger.addHandler(handler) ++ self.addCleanup(custom_logger.removeHandler, handler) ++ ++ response = HttpResponse(status=404) ++ log_response( ++ msg := "Handled by custom logger", ++ response=response, ++ request=self.request, ++ logger=custom_logger, ++ ) ++ ++ self.assertEqual( ++ f"WARNING:my.custom.logger:{msg}", log_stream.getvalue().strip() ++ ) diff --git a/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-2.patch b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-2.patch new file mode 100644 index 0000000000..2824a9c0e3 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-2.patch @@ -0,0 +1,225 @@ +From 88c0244497d492e84b4a2925ac4554698cdf179a Mon Sep 17 00:00:00 2001 +From: Natalia <124304+nessita@users.noreply.github.com> +Date: Mon, 19 May 2025 22:46:00 -0300 +Subject: [PATCH] [4.2.x] Added helpers in csrf_tests and logging_tests to + assert logs from `log_response()`. + +Backport of ad6f99889838ccc2c30b3c02ed3868c9b565e81b from main. + +CVE: CVE-2025-48432 +Upstream-Status: Backport [https://github.com/django/django/commit/32fd8dec5618bd09eccdeb9dbf512043193d68ef] +(cherry picked from commit 32fd8dec5618bd09eccdeb9dbf512043193d68ef) +Signed-off-by: Ankur Tyagi +--- + tests/csrf_tests/tests.py | 53 ++++++++++++++++++------------------ + tests/logging_tests/tests.py | 42 ++++++++++++++++++++-------- + 2 files changed, 57 insertions(+), 38 deletions(-) + +diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py +index ba8f87d6ac..b8d928151e 100644 +--- a/tests/csrf_tests/tests.py ++++ b/tests/csrf_tests/tests.py +@@ -1,3 +1,4 @@ ++import logging + import re + + from django.conf import settings +@@ -57,6 +58,21 @@ class CsrfFunctionTestMixin: + actual = _unmask_cipher_token(masked_secret) + self.assertEqual(actual, secret) + ++ def assertForbiddenReason( ++ self, response, logger_cm, reason, levelno=logging.WARNING ++ ): ++ self.assertEqual( ++ records_len := len(logger_cm.records), ++ 1, ++ f"Unexpected number of records for {logger_cm=} in {levelno=} (expected 1, " ++ f"got {records_len}).", ++ ) ++ record = logger_cm.records[0] ++ self.assertEqual(record.getMessage(), "Forbidden (%s): " % reason) ++ self.assertEqual(record.levelno, levelno) ++ self.assertEqual(record.status_code, 403) ++ self.assertEqual(response.status_code, 403) ++ + + class CsrfFunctionTests(CsrfFunctionTestMixin, SimpleTestCase): + def test_unmask_cipher_token(self): +@@ -347,8 +363,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + mw.process_request(req) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + resp = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(403, resp.status_code) +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % expected) ++ self.assertForbiddenReason(resp, cm, expected) + + def test_no_csrf_cookie(self): + """ +@@ -373,9 +388,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + mw.process_request(req) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + resp = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(403, resp.status_code) + self.assertEqual(resp["Content-Type"], "text/html; charset=utf-8") +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % expected) ++ self.assertForbiddenReason(resp, cm, expected) + + def test_csrf_cookie_bad_or_missing_token(self): + """ +@@ -480,18 +494,12 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + mw = CsrfViewMiddleware(post_form_view) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + resp = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(403, resp.status_code) +- self.assertEqual( +- cm.records[0].getMessage(), "Forbidden (%s): " % REASON_NO_CSRF_COOKIE +- ) ++ self.assertForbiddenReason(resp, cm, REASON_NO_CSRF_COOKIE) + + req = self._get_request(method="DELETE") + with self.assertLogs("django.security.csrf", "WARNING") as cm: + resp = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(403, resp.status_code) +- self.assertEqual( +- cm.records[0].getMessage(), "Forbidden (%s): " % REASON_NO_CSRF_COOKIE +- ) ++ self.assertForbiddenReason(resp, cm, REASON_NO_CSRF_COOKIE) + + def test_put_and_delete_allowed(self): + """ +@@ -879,11 +887,7 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + mw.process_request(req) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + resp = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(resp.status_code, 403) +- self.assertEqual( +- cm.records[0].getMessage(), +- "Forbidden (%s): " % REASON_CSRF_TOKEN_MISSING, +- ) ++ self.assertForbiddenReason(resp, cm, REASON_CSRF_TOKEN_MISSING) + + def test_reading_post_data_raises_os_error(self): + """ +@@ -908,9 +912,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + self.assertIs(mw._origin_verified(req), False) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + response = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(response.status_code, 403) + msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"] +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg) ++ self.assertForbiddenReason(response, cm, msg) + + @override_settings(ALLOWED_HOSTS=["www.example.com"]) + def test_bad_origin_null_origin(self): +@@ -923,9 +926,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + self.assertIs(mw._origin_verified(req), False) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + response = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(response.status_code, 403) + msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"] +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg) ++ self.assertForbiddenReason(response, cm, msg) + + @override_settings(ALLOWED_HOSTS=["www.example.com"]) + def test_bad_origin_bad_protocol(self): +@@ -939,9 +941,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + self.assertIs(mw._origin_verified(req), False) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + response = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(response.status_code, 403) + msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"] +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg) ++ self.assertForbiddenReason(response, cm, msg) + + @override_settings( + ALLOWED_HOSTS=["www.example.com"], +@@ -966,9 +967,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + self.assertIs(mw._origin_verified(req), False) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + response = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(response.status_code, 403) + msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"] +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg) ++ self.assertForbiddenReason(response, cm, msg) + self.assertEqual(mw.allowed_origins_exact, {"http://no-match.com"}) + self.assertEqual( + mw.allowed_origin_subdomains, +@@ -992,9 +992,8 @@ class CsrfViewMiddlewareTestMixin(CsrfFunctionTestMixin): + self.assertIs(mw._origin_verified(req), False) + with self.assertLogs("django.security.csrf", "WARNING") as cm: + response = mw.process_view(req, post_form_view, (), {}) +- self.assertEqual(response.status_code, 403) + msg = REASON_BAD_ORIGIN % req.META["HTTP_ORIGIN"] +- self.assertEqual(cm.records[0].getMessage(), "Forbidden (%s): " % msg) ++ self.assertForbiddenReason(response, cm, msg) + + @override_settings(ALLOWED_HOSTS=["www.example.com"]) + def test_good_origin_insecure(self): +diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py +index 2138a7fe50..4ffa49a1b8 100644 +--- a/tests/logging_tests/tests.py ++++ b/tests/logging_tests/tests.py +@@ -94,6 +94,28 @@ class DefaultLoggingTests( + + + class LoggingAssertionMixin: ++ ++ def assertLogRecord( ++ self, ++ logger_cm, ++ level, ++ msg, ++ status_code, ++ exc_class=None, ++ ): ++ self.assertEqual( ++ records_len := len(logger_cm.records), ++ 1, ++ f"Wrong number of calls for {logger_cm=} in {level=} (expected 1, got " ++ f"{records_len}).", ++ ) ++ record = logger_cm.records[0] ++ self.assertEqual(record.getMessage(), msg) ++ self.assertEqual(record.status_code, status_code) ++ if exc_class: ++ self.assertIsNotNone(record.exc_info) ++ self.assertEqual(record.exc_info[0], exc_class) ++ + def assertLogsRequest( + self, url, level, msg, status_code, logger="django.request", exc_class=None + ): +@@ -102,17 +124,7 @@ class LoggingAssertionMixin: + self.client.get(url) + except views.UncaughtException: + pass +- self.assertEqual( +- len(cm.records), +- 1, +- "Wrong number of calls for logger %r in %r level." % (logger, level), +- ) +- record = cm.records[0] +- self.assertEqual(record.getMessage(), msg) +- self.assertEqual(record.status_code, status_code) +- if exc_class: +- self.assertIsNotNone(record.exc_info) +- self.assertEqual(record.exc_info[0], exc_class) ++ self.assertLogRecord(cm, level, msg, status_code, exc_class) + + + @override_settings(DEBUG=True, ROOT_URLCONF="logging_tests.urls") +@@ -135,6 +147,14 @@ class HandlerLoggingTests( + msg="Not Found: /does_not_exist/", + ) + ++ async def test_async_page_not_found_warning(self): ++ logger = "django.request" ++ level = "WARNING" ++ with self.assertLogs(logger, level) as cm: ++ await self.async_client.get("/does_not_exist/") ++ ++ self.assertLogRecord(cm, level, "Not Found: /does_not_exist/", 404) ++ + def test_page_not_found_raised(self): + self.assertLogsRequest( + url="/does_not_exist_raised/", diff --git a/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-3.patch b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-3.patch new file mode 100644 index 0000000000..58550567a4 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-django-4.2.20/CVE-2025-48432-3.patch @@ -0,0 +1,164 @@ +From 5e137465b5b7668f9a32f5c4f4af374fd705f38d Mon Sep 17 00:00:00 2001 +From: Natalia <124304+nessita@users.noreply.github.com> +Date: Tue, 20 May 2025 15:29:52 -0300 +Subject: [PATCH] [4.2.x] Fixed CVE-2025-48432 -- Escaped formatting arguments + in `log_response()`. + +Suitably crafted requests containing a CRLF sequence in the request +path may have allowed log injection, potentially corrupting log files, +obscuring other attacks, misleading log post-processing tools, or +forging log entries. + +To mitigate this, all positional formatting arguments passed to the +logger are now escaped using "unicode_escape" encoding. + +Thanks to Seokchan Yoon (https://ch4n3.kr/) for the report. + +Co-authored-by: Carlton Gibson +Co-authored-by: Jake Howard + +Backport of a07ebec5591e233d8bbb38b7d63f35c5479eef0e from main. +CVE: CVE-2025-48432 +Upstream-Status: Backport [https://github.com/django/django/commit/ac03c5e7df8680c61cdb0d3bdb8be9095dba841e] +(cherry picked from commit ac03c5e7df8680c61cdb0d3bdb8be9095dba841e) +Signed-off-by: Ankur Tyagi +--- + django/utils/log.py | 7 +++- + tests/logging_tests/tests.py | 79 +++++++++++++++++++++++++++++++++++- + 2 files changed, 84 insertions(+), 2 deletions(-) + +diff --git a/django/utils/log.py b/django/utils/log.py +index fd0cc1bdc1..d7465f73d7 100644 +--- a/django/utils/log.py ++++ b/django/utils/log.py +@@ -238,9 +238,14 @@ def log_response( + else: + level = "info" + ++ escaped_args = tuple( ++ a.encode("unicode_escape").decode("ascii") if isinstance(a, str) else a ++ for a in args ++ ) ++ + getattr(logger, level)( + message, +- *args, ++ *escaped_args, + extra={ + "status_code": response.status_code, + "request": request, +diff --git a/tests/logging_tests/tests.py b/tests/logging_tests/tests.py +index 4ffa49a1b8..cda0a62f2c 100644 +--- a/tests/logging_tests/tests.py ++++ b/tests/logging_tests/tests.py +@@ -94,7 +94,6 @@ class DefaultLoggingTests( + + + class LoggingAssertionMixin: +- + def assertLogRecord( + self, + logger_cm, +@@ -147,6 +146,14 @@ class HandlerLoggingTests( + msg="Not Found: /does_not_exist/", + ) + ++ def test_control_chars_escaped(self): ++ self.assertLogsRequest( ++ url="/%1B[1;31mNOW IN RED!!!1B[0m/", ++ level="WARNING", ++ status_code=404, ++ msg=r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/", ++ ) ++ + async def test_async_page_not_found_warning(self): + logger = "django.request" + level = "WARNING" +@@ -155,6 +162,16 @@ class HandlerLoggingTests( + + self.assertLogRecord(cm, level, "Not Found: /does_not_exist/", 404) + ++ async def test_async_control_chars_escaped(self): ++ logger = "django.request" ++ level = "WARNING" ++ with self.assertLogs(logger, level) as cm: ++ await self.async_client.get(r"/%1B[1;31mNOW IN RED!!!1B[0m/") ++ ++ self.assertLogRecord( ++ cm, level, r"Not Found: /\x1b[1;31mNOW IN RED!!!1B[0m/", 404 ++ ) ++ + def test_page_not_found_raised(self): + self.assertLogsRequest( + url="/does_not_exist_raised/", +@@ -686,6 +703,7 @@ class LogResponseRealLoggerTests(TestCase): + self.assertEqual(record.levelno, levelno) + self.assertEqual(record.status_code, status_code) + self.assertEqual(record.request, request) ++ return record + + def test_missing_response_raises_attribute_error(self): + with self.assertRaises(AttributeError): +@@ -787,3 +805,62 @@ class LogResponseRealLoggerTests(TestCase): + self.assertEqual( + f"WARNING:my.custom.logger:{msg}", log_stream.getvalue().strip() + ) ++ ++ def test_unicode_escape_escaping(self): ++ test_cases = [ ++ # Control characters. ++ ("line\nbreak", "line\\nbreak"), ++ ("carriage\rreturn", "carriage\\rreturn"), ++ ("tab\tseparated", "tab\\tseparated"), ++ ("formfeed\f", "formfeed\\x0c"), ++ ("bell\a", "bell\\x07"), ++ ("multi\nline\ntext", "multi\\nline\\ntext"), ++ # Slashes. ++ ("slash\\test", "slash\\\\test"), ++ ("back\\slash", "back\\\\slash"), ++ # Quotes. ++ ('quote"test"', 'quote"test"'), ++ ("quote'test'", "quote'test'"), ++ # Accented, composed characters, emojis and symbols. ++ ("café", "caf\\xe9"), ++ ("e\u0301", "e\\u0301"), # e + combining acute ++ ("smile