mirror of
				https://github.com/django/django.git
				synced 2025-10-31 09:41:08 +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:
		| @@ -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) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user