1
0
mirror of https://github.com/django/django.git synced 2025-06-12 15:09:12 +00:00

[4.2.x] Refs CVE-2025-48432 -- Prevented log injection in remaining response logging.

Migrated remaining response-related logging to use the `log_response()`
helper to avoid potential log injection, to ensure untrusted values like
request paths are safely escaped.

Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>

Backport of 957951755259b412d5113333b32bf85871d29814 from main.
This commit is contained in:
Jake Howard 2025-06-04 16:08:46 +01:00 committed by Natalia
parent 10ba3f78da
commit b597d46bb1
4 changed files with 61 additions and 9 deletions

View File

@ -14,6 +14,7 @@ from django.template.response import TemplateResponse
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.functional import classproperty from django.utils.functional import classproperty
from django.utils.log import log_response
logger = logging.getLogger("django.request") logger = logging.getLogger("django.request")
@ -143,13 +144,14 @@ class View:
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
def http_method_not_allowed(self, request, *args, **kwargs): def http_method_not_allowed(self, request, *args, **kwargs):
logger.warning( response = HttpResponseNotAllowed(self._allowed_methods())
log_response(
"Method Not Allowed (%s): %s", "Method Not Allowed (%s): %s",
request.method, request.method,
request.path, request.path,
extra={"status_code": 405, "request": request}, response=response,
request=request,
) )
response = HttpResponseNotAllowed(self._allowed_methods())
if self.view_is_async: if self.view_is_async:
@ -261,10 +263,9 @@ class RedirectView(View):
else: else:
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
logger.warning( response = HttpResponseGone()
"Gone: %s", request.path, extra={"status_code": 410, "request": request} log_response("Gone: %s", request.path, response=response, request=request)
) return response
return HttpResponseGone()
def head(self, request, *args, **kwargs): def head(self, request, *args, **kwargs):
return self.get(request, *args, **kwargs) return self.get(request, *args, **kwargs)

14
docs/releases/4.2.23.txt Normal file
View File

@ -0,0 +1,14 @@
===========================
Django 4.2.23 release notes
===========================
*June 10, 2025*
Django 4.2.23 fixes a potential log injection issue in 4.2.22.
Bugfixes
========
* Fixed a log injection possibility by migrating remaining response logging
to ``django.utils.log.log_response()``, which safely escapes arguments such
as the request path to prevent unsafe log output (:cve:`2025-48432`).

View File

@ -26,6 +26,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
4.2.23
4.2.22 4.2.22
4.2.21 4.2.21
4.2.20 4.2.20

View File

@ -1,5 +1,8 @@
import logging
import time import time
from logging_tests.tests import LoggingAssertionMixin
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse from django.http import HttpResponse
from django.test import RequestFactory, SimpleTestCase, override_settings from django.test import RequestFactory, SimpleTestCase, override_settings
@ -63,7 +66,7 @@ class InstanceView(View):
return self return self
class ViewTest(SimpleTestCase): class ViewTest(LoggingAssertionMixin, SimpleTestCase):
rf = RequestFactory() rf = RequestFactory()
def _assert_simple(self, response): def _assert_simple(self, response):
@ -297,6 +300,25 @@ class ViewTest(SimpleTestCase):
response = view.dispatch(self.rf.head("/")) response = view.dispatch(self.rf.head("/"))
self.assertEqual(response.status_code, 405) self.assertEqual(response.status_code, 405)
def test_method_not_allowed_response_logged(self):
for path, escaped in [
("/foo/", "/foo/"),
(r"/%1B[1;31mNOW IN RED!!!1B[0m/", r"/\x1b[1;31mNOW IN RED!!!1B[0m/"),
]:
with self.subTest(path=path):
request = self.rf.get(path, REQUEST_METHOD="BOGUS")
with self.assertLogs("django.request", "WARNING") as handler:
response = SimpleView.as_view()(request)
self.assertLogRecord(
handler,
f"Method Not Allowed (BOGUS): {escaped}",
logging.WARNING,
405,
request,
)
self.assertEqual(response.status_code, 405)
@override_settings(ROOT_URLCONF="generic_views.urls") @override_settings(ROOT_URLCONF="generic_views.urls")
class TemplateViewTest(SimpleTestCase): class TemplateViewTest(SimpleTestCase):
@ -425,7 +447,7 @@ class TemplateViewTest(SimpleTestCase):
@override_settings(ROOT_URLCONF="generic_views.urls") @override_settings(ROOT_URLCONF="generic_views.urls")
class RedirectViewTest(SimpleTestCase): class RedirectViewTest(LoggingAssertionMixin, SimpleTestCase):
rf = RequestFactory() rf = RequestFactory()
def test_no_url(self): def test_no_url(self):
@ -549,6 +571,20 @@ class RedirectViewTest(SimpleTestCase):
response = view.dispatch(self.rf.head("/foo/")) response = view.dispatch(self.rf.head("/foo/"))
self.assertEqual(response.status_code, 410) self.assertEqual(response.status_code, 410)
def test_gone_response_logged(self):
for path, escaped in [
("/foo/", "/foo/"),
(r"/%1B[1;31mNOW IN RED!!!1B[0m/", r"/\x1b[1;31mNOW IN RED!!!1B[0m/"),
]:
with self.subTest(path=path):
request = self.rf.get(path)
with self.assertLogs("django.request", "WARNING") as handler:
RedirectView().dispatch(request)
self.assertLogRecord(
handler, f"Gone: {escaped}", logging.WARNING, 410, request
)
class GetContextDataTest(SimpleTestCase): class GetContextDataTest(SimpleTestCase):
def test_get_context_data_super(self): def test_get_context_data_super(self):