From 1ecc0a395be721e987e8e9fdfadde952b6dee1c7 Mon Sep 17 00:00:00 2001 From: Tom Hacohen Date: Fri, 4 Jan 2019 02:21:55 +0000 Subject: [PATCH] Fixed #30070, CVE-2019-3498 -- Fixed content spoofing possiblity in the default 404 page. Co-Authored-By: Tim Graham --- django/views/defaults.py | 9 ++++++--- docs/releases/1.11.18.txt | 18 ++++++++++++++++++ docs/releases/2.0.10.txt | 15 +++++++++++++-- docs/releases/2.1.5.txt | 14 ++++++++++++-- docs/releases/index.txt | 1 + tests/handlers/tests.py | 10 ++++++---- 6 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 docs/releases/1.11.18.txt diff --git a/django/views/defaults.py b/django/views/defaults.py index dee081ec3d..6c394490ab 100644 --- a/django/views/defaults.py +++ b/django/views/defaults.py @@ -1,3 +1,5 @@ +from urllib.parse import quote + from django.http import ( HttpResponseBadRequest, HttpResponseForbidden, HttpResponseNotFound, HttpResponseServerError, @@ -22,7 +24,8 @@ def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME): Templates: :template:`404.html` Context: request_path - The path of the requested URL (e.g., '/app/pages/bad_page/') + The path of the requested URL (e.g., '/app/pages/bad_page/'). It's + quoted to prevent a content injection attack. exception The message from the exception which triggered the 404 (if one was supplied), or the exception class name @@ -38,7 +41,7 @@ def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME): if isinstance(message, str): exception_repr = message context = { - 'request_path': request.path, + 'request_path': quote(request.path), 'exception': exception_repr, } try: @@ -51,7 +54,7 @@ def page_not_found(request, exception, template_name=ERROR_404_TEMPLATE_NAME): raise template = Engine().from_string( '

Not Found

' - '

The requested URL {{ request_path }} was not found on this server.

') + '

The requested resource was not found on this server.

') body = template.render(Context(context)) content_type = 'text/html' return HttpResponseNotFound(body, content_type=content_type) diff --git a/docs/releases/1.11.18.txt b/docs/releases/1.11.18.txt new file mode 100644 index 0000000000..82a229e6dd --- /dev/null +++ b/docs/releases/1.11.18.txt @@ -0,0 +1,18 @@ +============================ +Django 1.11.18 release notes +============================ + +*January 4, 2019* + +Django 1.11.18 fixes a security issue in 1.11.17. + +CVE-2019-3498: Content spoofing possibility in the default 404 page +------------------------------------------------------------------- + +An attacker could craft a malicious URL that could make spoofed content appear +on the default page generated by the ``django.views.defaults.page_not_found()`` +view. + +The URL path is no longer displayed in the default 404 template and the +``request_path`` context variable is now quoted to fix the issue for custom +templates that use the path. diff --git a/docs/releases/2.0.10.txt b/docs/releases/2.0.10.txt index 18901490e0..8b0bf3a2a2 100644 --- a/docs/releases/2.0.10.txt +++ b/docs/releases/2.0.10.txt @@ -2,9 +2,20 @@ Django 2.0.10 release notes =========================== -*Release date TBD* +*January 4, 2019* -Django 2.0.10 fixes several bugs in 2.0.9. +Django 2.0.10 fixes a security issue and several bugs in 2.0.9. + +CVE-2019-3498: Content spoofing possibility in the default 404 page +------------------------------------------------------------------- + +An attacker could craft a malicious URL that could make spoofed content appear +on the default page generated by the ``django.views.defaults.page_not_found()`` +view. + +The URL path is no longer displayed in the default 404 template and the +``request_path`` context variable is now quoted to fix the issue for custom +templates that use the path. Bugfixes ======== diff --git a/docs/releases/2.1.5.txt b/docs/releases/2.1.5.txt index 27ffbc7510..ebe775a3d3 100644 --- a/docs/releases/2.1.5.txt +++ b/docs/releases/2.1.5.txt @@ -2,10 +2,20 @@ Django 2.1.5 release notes ========================== -*Expected January 1, 2019* +*January 4, 2019* +Django 2.1.5 fixes a security issue and several bugs in 2.1.4. -Django 2.1.5 fixes several bugs in 2.1.4. +CVE-2019-3498: Content spoofing possibility in the default 404 page +------------------------------------------------------------------- + +An attacker could craft a malicious URL that could make spoofed content appear +on the default page generated by the ``django.views.defaults.page_not_found()`` +view. + +The URL path is no longer displayed in the default 404 template and the +``request_path`` context variable is now quoted to fix the issue for custom +templates that use the path. Bugfixes ======== diff --git a/docs/releases/index.txt b/docs/releases/index.txt index f292a4d566..239d27b3ec 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -61,6 +61,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 1.11.18 1.11.17 1.11.16 1.11.15 diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 70f0e875a5..fc7074833b 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -5,6 +5,7 @@ from django.db import close_old_connections, connection from django.test import ( RequestFactory, SimpleTestCase, TransactionTestCase, override_settings, ) +from django.utils.version import PY37 class HandlerTests(SimpleTestCase): @@ -162,16 +163,17 @@ class HandlerRequestTests(SimpleTestCase): def test_invalid_urls(self): response = self.client.get('~%A9helloworld') - self.assertContains(response, '~%A9helloworld', status_code=404) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.context['request_path'], '/~%25A9helloworld' if PY37 else '/%7E%25A9helloworld') response = self.client.get('d%aao%aaw%aan%aal%aao%aaa%aad%aa/') - self.assertContains(response, 'd%AAo%AAw%AAn%AAl%AAo%AAa%AAd%AA', status_code=404) + self.assertEqual(response.context['request_path'], '/d%25AAo%25AAw%25AAn%25AAl%25AAo%25AAa%25AAd%25AA') response = self.client.get('/%E2%99%E2%99%A5/') - self.assertContains(response, '%E2%99\u2665', status_code=404) + self.assertEqual(response.context['request_path'], '/%25E2%2599%E2%99%A5/') response = self.client.get('/%E2%98%8E%E2%A9%E2%99%A5/') - self.assertContains(response, '\u260e%E2%A9\u2665', status_code=404) + self.assertEqual(response.context['request_path'], '/%E2%98%8E%25E2%25A9%E2%99%A5/') def test_environ_path_info_type(self): environ = self.request_factory.get('/%E2%A8%87%87%A5%E2%A8%A0').environ