From 74e9c071e27e418955a694a052ba6e58f57fb1f8 Mon Sep 17 00:00:00 2001
From: Tom Hacohen <tasn@users.noreply.github.com>
Date: Fri, 4 Jan 2019 02:21:55 +0000
Subject: [PATCH 1/1] [1.11.x] Fixed #30070, CVE-2019-3498 -- Fixed content
 spoofing possiblity in the default 404 page.

Co-Authored-By: Tim Graham <timograham@gmail.com>
Backport of 1ecc0a395be721e987e8e9fdfadde952b6dee1c7 from master.

(cherry picked from commit 1cd00fcf52d089ef0fe03beabd05d59df8ea052a)
---
 django/views/defaults.py | 26 +++++++++++++++++++++++---
 tests/handlers/tests.py  | 23 +++++++++++++++--------
 2 files changed, 38 insertions(+), 11 deletions(-)

diff --git a/django/views/defaults.py b/django/views/defaults.py
index 35d42830a4..bcd44cd074 100644
--- a/django/views/defaults.py
+++ b/django/views/defaults.py
@@ -1,5 +1,8 @@
 from django import http
 from django.template import Context, Engine, TemplateDoesNotExist, loader
+from django.utils import six
+from django.utils.encoding import force_text
+from django.utils.http import urlquote
 from django.views.decorators.csrf import requires_csrf_token
 
 
@@ -14,9 +17,26 @@ def page_not_found(request, template_name='404.html'):
     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
     """
-    context = {'request_path': request.path}
+    exception_repr = exception.__class__.__name__
+    # Try to get an "interesting" exception message, if any (and not the ugly
+    # Resolver404 dictionary)
+    try:
+        message = exception.args[0]
+    except (AttributeError, IndexError):
+        pass
+    else:
+        if isinstance(message, six.text_type):
+            exception_repr = message
+    context = {
+        'request_path': urlquote(request.path),
+        'exception': exception_repr,
+    }
     try:
         template = loader.get_template(template_name)
         body = template.render(context, request)
@@ -24,7 +44,7 @@ def page_not_found(request, template_name='404.html'):
     except TemplateDoesNotExist:
         template = Engine().from_string(
             '<h1>Not Found</h1>'
-            '<p>The requested URL {{ request_path }} was not found on this server.</p>')
+            '<p>The requested resource was not found on this server.</p>')
         body = template.render(Context(context))
         content_type = 'text/html'
     return http.HttpResponseNotFound(body, content_type=content_type)
diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py
index a1fed74105..8ebae0eeae 100644
--- a/tests/handlers/tests.py
+++ b/tests/handlers/tests.py
@@ -2,7 +2,11 @@
 
 from __future__ import unicode_literals
 
-from django.core.handlers.wsgi import WSGIHandler, WSGIRequest
+import sys
+import unittest
+
+from django.core.exceptions import ImproperlyConfigured
+from django.core.handlers.wsgi import WSGIHandler, WSGIRequest, get_script_name
 from django.core.signals import request_finished, request_started
 from django.db import close_old_connections, connection
 from django.test import (
@@ -11,6 +15,12 @@ from django.test import (
 from django.utils import six
 from django.utils.encoding import force_str
 
+try:
+    from http import HTTPStatus
+except ImportError:  # Python < 3.5
+    HTTPStatus = None
+
+PY37 = sys.version_info >= (3, 7, 0)
 
 class HandlerTests(TestCase):
 
@@ -183,19 +193,16 @@ class HandlerNotFoundTest(TestCase):
     def test_invalid_urls(self):
         response = self.client.get('~%A9helloworld')
         self.assertEqual(response.status_code, 404)
-        self.assertContains(response, '~%A9helloworld', 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.assertEqual(response.status_code, 404)
-        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.assertEqual(response.status_code, 404)
-        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.assertEqual(response.status_code, 404)
-        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 = RequestFactory().get('/%E2%A8%87%87%A5%E2%A8%A0').environ
-- 
2.16.4

