mirror of
https://github.com/django/django.git
synced 2025-06-11 22:49:13 +00:00
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 <carlton@noumenal.es> Co-authored-by: Jake Howard <git@theorangeone.net>
This commit is contained in:
parent
08187c94ed
commit
a07ebec559
@ -245,9 +245,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,
|
||||
|
@ -5,3 +5,17 @@ Django 4.2.22 release notes
|
||||
*June 4, 2025*
|
||||
|
||||
Django 4.2.22 fixes a security issue with severity "low" in 4.2.21.
|
||||
|
||||
CVE-2025-48432: Potential log injection via unescaped request path
|
||||
==================================================================
|
||||
|
||||
Internal HTTP response logging used ``request.path`` directly, allowing control
|
||||
characters (e.g. newlines or ANSI escape sequences) to be written unescaped
|
||||
into logs. This could enable log injection or forgery, letting attackers
|
||||
manipulate log appearance or structure, especially in logs processed by
|
||||
external systems or viewed in terminals.
|
||||
|
||||
Although this does not directly impact Django's security model, it poses risks
|
||||
when logs are consumed or interpreted by other tools. To fix this, the internal
|
||||
``django.utils.log.log_response()`` function now escapes all positional
|
||||
formatting arguments using a safe encoding.
|
||||
|
@ -5,3 +5,17 @@ Django 5.1.10 release notes
|
||||
*June 4, 2025*
|
||||
|
||||
Django 5.1.10 fixes a security issue with severity "low" in 5.1.9.
|
||||
|
||||
CVE-2025-48432: Potential log injection via unescaped request path
|
||||
==================================================================
|
||||
|
||||
Internal HTTP response logging used ``request.path`` directly, allowing control
|
||||
characters (e.g. newlines or ANSI escape sequences) to be written unescaped
|
||||
into logs. This could enable log injection or forgery, letting attackers
|
||||
manipulate log appearance or structure, especially in logs processed by
|
||||
external systems or viewed in terminals.
|
||||
|
||||
Although this does not directly impact Django's security model, it poses risks
|
||||
when logs are consumed or interpreted by other tools. To fix this, the internal
|
||||
``django.utils.log.log_response()`` function now escapes all positional
|
||||
formatting arguments using a safe encoding.
|
||||
|
@ -7,6 +7,20 @@ Django 5.2.2 release notes
|
||||
Django 5.2.2 fixes a security issue with severity "low" and several bugs in
|
||||
5.2.1.
|
||||
|
||||
CVE-2025-48432: Potential log injection via unescaped request path
|
||||
==================================================================
|
||||
|
||||
Internal HTTP response logging used ``request.path`` directly, allowing control
|
||||
characters (e.g. newlines or ANSI escape sequences) to be written unescaped
|
||||
into logs. This could enable log injection or forgery, letting attackers
|
||||
manipulate log appearance or structure, especially in logs processed by
|
||||
external systems or viewed in terminals.
|
||||
|
||||
Although this does not directly impact Django's security model, it poses risks
|
||||
when logs are consumed or interpreted by other tools. To fix this, the internal
|
||||
``django.utils.log.log_response()`` function now escapes all positional
|
||||
formatting arguments using a safe encoding.
|
||||
|
||||
Bugfixes
|
||||
========
|
||||
|
||||
|
@ -147,6 +147,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 +163,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/",
|
||||
@ -705,6 +723,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):
|
||||
@ -806,3 +825,64 @@ 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🙂", "smile\\U0001f642"),
|
||||
("weird ☃️", "weird \\u2603\\ufe0f"),
|
||||
# Non-Latin alphabets.
|
||||
("Привет", "\\u041f\\u0440\\u0438\\u0432\\u0435\\u0442"),
|
||||
("你好", "\\u4f60\\u597d"),
|
||||
# ANSI escape sequences.
|
||||
("escape\x1b[31mred\x1b[0m", "escape\\x1b[31mred\\x1b[0m"),
|
||||
(
|
||||
"/\x1b[1;31mCAUTION!!YOU ARE PWNED\x1b[0m/",
|
||||
"/\\x1b[1;31mCAUTION!!YOU ARE PWNED\\x1b[0m/",
|
||||
),
|
||||
(
|
||||
"/\r\n\r\n1984-04-22 INFO Listening on 0.0.0.0:8080\r\n\r\n",
|
||||
"/\\r\\n\\r\\n1984-04-22 INFO Listening on 0.0.0.0:8080\\r\\n\\r\\n",
|
||||
),
|
||||
# Plain safe input.
|
||||
("normal-path", "normal-path"),
|
||||
("slash/colon:", "slash/colon:"),
|
||||
# Non strings.
|
||||
(0, "0"),
|
||||
([1, 2, 3], "[1, 2, 3]"),
|
||||
({"test": "🙂"}, "{'test': '🙂'}"),
|
||||
]
|
||||
|
||||
msg = "Test message: %s"
|
||||
for case, expected in test_cases:
|
||||
with (
|
||||
self.assertLogs("django.request", level="ERROR") as cm,
|
||||
self.subTest(case=case),
|
||||
):
|
||||
response = HttpResponse(status=318)
|
||||
log_response(msg, case, response=response, level="error")
|
||||
|
||||
record = self.assertResponseLogged(
|
||||
cm,
|
||||
msg % expected,
|
||||
levelno=logging.ERROR,
|
||||
status_code=318,
|
||||
request=None,
|
||||
)
|
||||
# Log record is always a single line.
|
||||
self.assertEqual(len(record.getMessage().splitlines()), 1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user