File CVE-2022-39348-do-not-echo-host-header.patch of Package python-Twisted.31485

From 869fbe6b2cc1f7b803085c51b69e0d1f23a6d80b Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Thu, 20 Oct 2022 23:19:53 -0700
Subject: [PATCH 01/12] Deprecate twisted.web.resource.ErrorPage and spawn

---
 src/twisted/web/newsfragments/11716.feature |  1 +
 src/twisted/web/newsfragments/11716.removal |  1 +
 src/twisted/web/resource.py                 | 69 +++++++++++++++++----
 src/twisted/web/test/test_resource.py       | 51 +++++++++++++--
 4 files changed, 106 insertions(+), 16 deletions(-)
 create mode 100644 src/twisted/web/newsfragments/11716.feature
 create mode 100644 src/twisted/web/newsfragments/11716.removal

Index: Twisted-19.10.0/src/twisted/web/newsfragments/11716.feature
===================================================================
--- /dev/null
+++ Twisted-19.10.0/src/twisted/web/newsfragments/11716.feature
@@ -0,0 +1 @@
+The twisted.web.pages.errorPage, notFound, and forbidden each return an IResource that displays an HTML error pages safely rendered using twisted.web.template.
Index: Twisted-19.10.0/src/twisted/web/newsfragments/11716.removal
===================================================================
--- /dev/null
+++ Twisted-19.10.0/src/twisted/web/newsfragments/11716.removal
@@ -0,0 +1 @@
+The twisted.web.resource.ErrorPage, NoResource, and ForbiddenResource classes have been deprecated in favor of new implementations twisted.web.pages module because they permit HTML injection.
Index: Twisted-19.10.0/src/twisted/web/resource.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/resource.py
+++ Twisted-19.10.0/src/twisted/web/resource.py
@@ -1,9 +1,11 @@
-# -*- test-case-name: twisted.web.test.test_web -*-
+# -*- test-case-name: twisted.web.test.test_web, twisted.web.test.test_resource -*-
 # Copyright (c) Twisted Matrix Laboratories.
 # See LICENSE for details.
 
 """
 Implementation of the lowest-level Resource class.
+
+See L{twisted.web.pages} for some utility implementations.
 """
 
 from __future__ import division, absolute_import
@@ -17,9 +19,12 @@ import warnings
 
 from zope.interface import Attribute, Interface, implementer
 
+from incremental import Version
+
 from twisted.python.compat import nativeString, unicode
 from twisted.python.reflect import prefixedMethodNames
 from twisted.python.components import proxyForInterface
+from twisted.python.deprecate import deprecatedModuleAttribute
 
 from twisted.web._responses import FORBIDDEN, NOT_FOUND
 from twisted.web.error import UnsupportedMethod
@@ -179,7 +184,7 @@ class Resource:
         Parameters and return value have the same meaning and requirements as
         those defined by L{IResource.getChildWithDefault}.
         """
-        return NoResource("No such child resource.")
+        return _UnsafeNoResource()
 
 
     def getChildWithDefault(self, path, request):
@@ -292,20 +297,25 @@ def _computeAllowedMethods(resource):
 
 
 
-class ErrorPage(Resource):
+class _UnsafeErrorPage(Resource):
     """
-    L{ErrorPage} is a resource which responds with a particular
+    L{_UnsafeErrorPage}, publicly available via the deprecated alias
+    C{ErrorPage}, is a resource which responds with a particular
     (parameterized) status and a body consisting of HTML containing some
     descriptive text.  This is useful for rendering simple error pages.
 
+    Deprecated in Twisted NEXT because it permits HTML injection; use
+    L{twisted.web.pages.errorPage} instead.
+
     @ivar template: A native string which will have a dictionary interpolated
         into it to generate the response body.  The dictionary has the following
         keys:
 
-          - C{"code"}: The status code passed to L{ErrorPage.__init__}.
-          - C{"brief"}: The brief description passed to L{ErrorPage.__init__}.
+          - C{"code"}: The status code passed to L{_UnsafeErrorPage.__init__}.
+          - C{"brief"}: The brief description passed to
+            L{_UnsafeErrorPage.__init__}.
           - C{"detail"}: The detailed description passed to
-            L{ErrorPage.__init__}.
+            L{_UnsafeErrorPage.__init__}.
 
     @ivar code: An integer status code which will be used for the response.
     @type code: C{int}
@@ -350,24 +360,57 @@ class ErrorPage(Resource):
 
 
 
-class NoResource(ErrorPage):
+class _UnsafeNoResource(_UnsafeErrorPage):
     """
-    L{NoResource} is a specialization of L{ErrorPage} which returns the HTTP
-    response code I{NOT FOUND}.
+    L{_UnsafeNoResource}, publicly available via the deprecated alias
+    C{NoResource}, is a specialization of L{_UnsafeErrorPage} which
+    returns the HTTP response code I{NOT FOUND}.
+
+    Deprecated in Twisted NEXT because it permits HTML injection; use
+    L{twisted.web.pages.notFound} instead.
     """
     def __init__(self, message="Sorry. No luck finding that resource."):
-        ErrorPage.__init__(self, NOT_FOUND, "No Such Resource", message)
+        _UnsafeErrorPage.__init__(self, NOT_FOUND, "No Such Resource", message)
 
 
-
-class ForbiddenResource(ErrorPage):
+class _UnsafeForbiddenResource(_UnsafeErrorPage):
     """
-    L{ForbiddenResource} is a specialization of L{ErrorPage} which returns the
-    I{FORBIDDEN} HTTP response code.
+    L{_UnsafeForbiddenResource}, publicly available via the deprecated alias
+    C{ForbiddenResource} is a specialization of L{_UnsafeErrorPage} which
+    returns the I{FORBIDDEN} HTTP response code.
+
+    Deprecated in Twisted NEXT because it permits HTML injection; use
+    L{twisted.web.pages.forbidden} instead.
     """
     def __init__(self, message="Sorry, resource is forbidden."):
-        ErrorPage.__init__(self, FORBIDDEN, "Forbidden Resource", message)
+        _UnsafeErrorPage.__init__(self, FORBIDDEN, "Forbidden Resource", message)
+
 
+# Deliberately undocumented public aliases. See GHSA-vg46-2rrj-3647.
+ErrorPage = _UnsafeErrorPage
+NoResource = _UnsafeNoResource
+ForbiddenResource = _UnsafeForbiddenResource
+
+deprecatedModuleAttribute(
+    Version("Twisted", "NEXT", 0, 0),
+    "Use twisted.web.pages.errorPage instead, which properly escapes HTML.",
+    __name__,
+    "ErrorPage",
+)
+
+deprecatedModuleAttribute(
+    Version("Twisted", "NEXT", 0, 0),
+    "Use twisted.web.pages.notFound instead, which properly escapes HTML.",
+    __name__,
+    "NoResource",
+)
+
+deprecatedModuleAttribute(
+    Version("Twisted", "NEXT", 0, 0),
+    "Use twisted.web.pages.forbidden instead, which properly escapes HTML.",
+    __name__,
+    "ForbiddenResource",
+)
 
 
 class _IEncodingResource(Interface):
Index: Twisted-19.10.0/src/twisted/web/test/test_resource.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/test/test_resource.py
+++ Twisted-19.10.0/src/twisted/web/test/test_resource.py
@@ -10,7 +10,10 @@ from twisted.python.compat import _PY3
 
 from twisted.web.error import UnsupportedMethod
 from twisted.web.resource import (
-    NOT_FOUND, FORBIDDEN, Resource, ErrorPage, NoResource, ForbiddenResource,
+    NOT_FOUND, FORBIDDEN, Resource,
+    _UnsafeErrorPage as ErrorPage,
+    _UnsafeForbiddenResource as ForbiddenResource,
+    _UnsafeNoResource as NoResource,
     getChildForRequest)
 from twisted.web.http_headers import Headers
 from twisted.web.test.requesthelper import DummyRequest
@@ -18,13 +21,56 @@ from twisted.web.test.requesthelper impo
 
 class ErrorPageTests(TestCase):
     """
-    Tests for L{ErrorPage}, L{NoResource}, and L{ForbiddenResource}.
+    Tests for L{_UnafeErrorPage}, L{_UnsafeNoResource}, and
+    L{_UnsafeForbiddenResource}.
     """
 
     errorPage = ErrorPage
     noResource = NoResource
     forbiddenResource = ForbiddenResource
 
+    def test_deprecatedErrorPage(self):
+        """
+        The public C{twisted.web.resource.ErrorPage} alias for the
+        corresponding C{_Unsafe} class produces a deprecation warning when
+        imported.
+        """
+        from twisted.web.resource import ErrorPage
+
+        self.assertIs(ErrorPage, self.errorPage)
+
+        [warning] = self.flushWarnings()
+        self.assertEqual(warning["category"], DeprecationWarning)
+        self.assertIn("twisted.web.pages.errorPage", warning["message"])
+
+    def test_deprecatedNoResource(self):
+        """
+        The public C{twisted.web.resource.NoResource} alias for the
+        corresponding C{_Unsafe} class produces a deprecation warning when
+        imported.
+        """
+        from twisted.web.resource import NoResource
+
+        self.assertIs(NoResource, self.noResource)
+
+        [warning] = self.flushWarnings()
+        self.assertEqual(warning["category"], DeprecationWarning)
+        self.assertIn("twisted.web.pages.notFound", warning["message"])
+
+    def test_deprecatedForbiddenResource(self):
+        """
+        The public C{twisted.web.resource.ForbiddenResource} alias for the
+        corresponding C{_Unsafe} class produce a deprecation warning when
+        imported.
+        """
+        from twisted.web.resource import ForbiddenResource
+
+        self.assertIs(ForbiddenResource, self.forbiddenResource)
+
+        [warning] = self.flushWarnings()
+        self.assertEqual(warning["category"], DeprecationWarning)
+        self.assertIn("twisted.web.pages.forbidden", warning["message"])
+
     def test_getChild(self):
         """
         The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
Index: Twisted-19.10.0/src/twisted/web/pages.py
===================================================================
--- /dev/null
+++ Twisted-19.10.0/src/twisted/web/pages.py
@@ -0,0 +1,134 @@
+# -*- test-case-name: twisted.web.test.test_pages -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Utility implementations of L{IResource}.
+"""
+
+__all__ = (
+    "errorPage",
+    "notFound",
+    "forbidden",
+)
+
+from typing import cast
+
+from twisted.web import http
+from twisted.web.iweb import IRenderable, IRequest
+from twisted.web.resource import IResource, Resource
+from twisted.web.template import renderElement, tags
+
+
+class _ErrorPage(Resource):
+    """
+    L{_ErrorPage} is a resource that responds to all requests with a particular
+    (parameterized) HTTP status code and an HTML body containing some
+    descriptive text. This is useful for rendering simple error pages.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @ivar _code: An integer HTTP status code which will be used for the
+        response.
+
+    @ivar _brief: A short string which will be included in the response body as
+        the page title.
+
+    @ivar _detail: A longer string which will be included in the response body.
+    """
+
+    def __init__(self, code: int, brief: str, detail: str) -> None:
+        super().__init__()
+        self._code: int = code
+        self._brief: str = brief
+        self._detail: str = detail
+
+    def render(self, request: IRequest) -> object:
+        """
+        Respond to all requests with the given HTTP status code and an HTML
+        document containing the explanatory strings.
+        """
+        request.setResponseCode(self._code)
+        request.setHeader(b"content-type", b"text/html; charset=utf-8")
+        return renderElement(
+            request,
+            # cast because the type annotations here seem off; Tag isn't an
+            # IRenderable but also probably should be? See
+            # https://github.com/twisted/twisted/issues/4982
+            cast(
+                IRenderable,
+                tags.html(
+                    tags.head(tags.title(f"{self._code} - {self._brief}")),
+                    tags.body(tags.h1(self._brief), tags.p(self._detail)),
+                ),
+            ),
+        )
+
+    def getChild(self, path: bytes, request: IRequest) -> Resource:
+        """
+        Handle all requests for which L{_ErrorPage} lacks a child by returning
+        this error page.
+
+        @param path: A path segment.
+
+        @param request: HTTP request
+        """
+        return self
+
+
+def errorPage(code: int, brief: str, detail: str) -> IResource:
+    """
+    Build a resource that responds to all requests with a particular HTTP
+    status code and an HTML body containing some descriptive text. This is
+    useful for rendering simple error pages.
+
+    The resource dynamically handles all paths below it. Use
+    L{IResource.putChild()} override specific path.
+
+    @param code: An integer HTTP status code which will be used for the
+        response.
+
+    @param brief: A short string which will be included in the response
+        body as the page title.
+
+    @param detail: A longer string which will be included in the
+        response body.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(code, brief, detail)
+
+
+def notFound(
+    brief: str = "No Such Resource",
+    message: str = "Sorry. No luck finding that resource.",
+) -> IResource:
+    """
+    Generate an L{IResource} with a 404 Not Found status code.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @param brief: A short string displayed as the page title.
+
+    @param brief: A longer string displayed in the page body.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(http.NOT_FOUND, brief, message)
+
+
+def forbidden(
+    brief: str = "Forbidden Resource", message: str = "Sorry, resource is forbidden."
+) -> IResource:
+    """
+    Generate an L{IResource} with a 403 Forbidden status code.
+
+    @see: L{twisted.web.pages.errorPage}
+
+    @param brief: A short string displayed as the page title.
+
+    @param brief: A longer string displayed in the page body.
+
+    @returns: An L{IResource}
+    """
+    return _ErrorPage(http.FORBIDDEN, brief, message)
Index: Twisted-19.10.0/src/twisted/web/test/test_pages.py
===================================================================
--- /dev/null
+++ Twisted-19.10.0/src/twisted/web/test/test_pages.py
@@ -0,0 +1,113 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Test L{twisted.web.pages}
+"""
+
+from typing import cast
+
+from twisted.trial.unittest import SynchronousTestCase
+from twisted.web.http_headers import Headers
+from twisted.web.iweb import IRequest
+from twisted.web.pages import errorPage, forbidden, notFound
+from twisted.web.resource import IResource
+from twisted.web.test.requesthelper import DummyRequest
+
+
+def _render(resource: IResource) -> DummyRequest:
+    """
+    Render a response using the given resource.
+
+    @param resource: The resource to use to handle the request.
+
+    @returns: The request that the resource handled,
+    """
+    request = DummyRequest([b""])
+    # The cast is necessary because DummyRequest isn't annotated
+    # as an IRequest, and this can't be trivially done. See
+    # https://github.com/twisted/twisted/issues/11719
+    resource.render(cast(IRequest, request))
+    return request
+
+
+class ErrorPageTests(SynchronousTestCase):
+    """
+    Test L{twisted.web.pages._ErrorPage} and its public aliases L{errorPage},
+    L{notFound} and L{forbidden}.
+    """
+
+    maxDiff = None
+
+    def assertResponse(self, request: DummyRequest, code: int, body: bytes) -> None:
+        self.assertEqual(request.responseCode, code)
+        self.assertEqual(
+            request.responseHeaders,
+            Headers({b"content-type": [b"text/html; charset=utf-8"]}),
+        )
+        self.assertEqual(
+            # Decode to str because unittest somehow still doesn't diff bytes
+            # without truncating them in 2022.
+            b"".join(request.written).decode("latin-1"),
+            body.decode("latin-1"),
+        )
+
+    def test_escapesHTML(self):
+        """
+        The I{brief} and I{detail} parameters are HTML-escaped on render.
+        """
+        self.assertResponse(
+            _render(errorPage(400, "A & B", "<script>alert('oops!')")),
+            400,
+            (
+                b"<!DOCTYPE html>\n"
+                b"<html><head><title>400 - A &amp; B</title></head>"
+                b"<body><h1>A &amp; B</h1><p>&lt;script&gt;alert('oops!')"
+                b"</p></body></html>"
+            ),
+        )
+
+    def test_getChild(self):
+        """
+        The C{getChild} method of the resource returned by L{errorPage} returns
+        the L{_ErrorPage} it is called on.
+        """
+        page = errorPage(404, "foo", "bar")
+        self.assertIs(
+            page.getChild(b"name", DummyRequest([b""])),
+            page,
+        )
+
+    def test_notFoundDefaults(self):
+        """
+        The default arguments to L{twisted.web.pages.notFound} produce
+        a reasonable error page.
+        """
+        self.assertResponse(
+            _render(notFound()),
+            404,
+            (
+                b"<!DOCTYPE html>\n"
+                b"<html><head><title>404 - No Such Resource</title></head>"
+                b"<body><h1>No Such Resource</h1>"
+                b"<p>Sorry. No luck finding that resource.</p>"
+                b"</body></html>"
+            ),
+        )
+
+    def test_forbiddenDefaults(self):
+        """
+        The default arguments to L{twisted.web.pages.forbidden} produce
+        a reasonable error page.
+        """
+        self.assertResponse(
+            _render(forbidden()),
+            403,
+            (
+                b"<!DOCTYPE html>\n"
+                b"<html><head><title>403 - Forbidden Resource</title></head>"
+                b"<body><h1>Forbidden Resource</h1>"
+                b"<p>Sorry, resource is forbidden.</p>"
+                b"</body></html>"
+            ),
+        )
Index: Twisted-19.10.0/src/twisted/web/_auth/wrapper.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/_auth/wrapper.py
+++ Twisted-19.10.0/src/twisted/web/_auth/wrapper.py
@@ -20,7 +20,7 @@ from twisted.cred.credentials import Ano
 from twisted.python.compat import unicode
 from twisted.python.components import proxyForInterface
 from twisted.web import util
-from twisted.web.resource import ErrorPage, IResource
+from twisted.web.resource import IResource, _UnsafeErrorPage
 from twisted.logger import Logger
 
 from zope.interface import implementer
@@ -54,7 +54,8 @@ class UnauthorizedResource(object):
             return b" ".join([scheme, b", ".join(l)])
 
         def quoteString(s):
-            return b'"' + s.replace(b'\\', b'\\\\').replace(b'"', b'\\"') + b'"'
+            return b'"' + s.replace(b"\\", rb"\\").replace(b'"', rb"\"") + b'"'
+
 
         request.setResponseCode(401)
         for fact in self._credentialFactories:
@@ -125,7 +126,7 @@ class HTTPAuthSessionWrapper(object):
             return UnauthorizedResource(self._credentialFactories)
         except:
             self._log.failure("Unexpected failure from credentials factory")
-            return ErrorPage(500, None, None)
+            return _UnsafeErrorPage(500, "Internal Error", "")
         else:
             return util.DeferredResource(self._login(credentials))
 
@@ -216,7 +217,7 @@ class HTTPAuthSessionWrapper(object):
                 "unexpected error",
                 failure=result,
             )
-            return ErrorPage(500, None, None)
+            return _UnsafeErrorPage(500, "Internal Error", "")
 
 
     def _selectParseHeader(self, header):
Index: Twisted-19.10.0/src/twisted/web/distrib.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/distrib.py
+++ Twisted-19.10.0/src/twisted/web/distrib.py
@@ -123,11 +123,12 @@ class Issue:
         #XXX: Argh. FIXME.
         failure = str(failure)
         self.request.write(
-            resource.ErrorPage(http.INTERNAL_SERVER_ERROR,
-                               "Server Connection Lost",
-                               "Connection to distributed server lost:" +
-                               util._PRE(failure)).
-            render(self.request))
+            resource._UnsafeErrorPage(
+                http.INTERNAL_SERVER_ERROR,
+                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
+                "Server Connection Lost",
+                "Connection to distributed server lost:" + util._PRE(failure)
+            ).render(self.request))
         self.request.finish()
         self._log.info(failure)
 
@@ -373,7 +374,7 @@ class UserDirectory(resource.Resource):
             pw_name, pw_passwd, pw_uid, pw_gid, pw_gecos, pw_dir, pw_shell \
                      = self._pwd.getpwnam(username)
         except KeyError:
-            return resource.NoResource()
+            return resource._UnsafeNoResource()
         if sub:
             twistdsock = os.path.join(pw_dir, self.userSocketName)
             rs = ResourceSubscription('unix',twistdsock)
@@ -382,5 +383,5 @@ class UserDirectory(resource.Resource):
         else:
             path = os.path.join(pw_dir, self.userDirName)
             if not os.path.exists(path):
-                return resource.NoResource()
+                return resource._UnsafeNoResource()
             return static.File(path)
Index: Twisted-19.10.0/src/twisted/web/script.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/script.py
+++ Twisted-19.10.0/src/twisted/web/script.py
@@ -46,7 +46,7 @@ class CacheScanner:
     def recache(self):
         self.doCache = 1
 
-noRsrc = resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource)
+noRsrc = resource._UnsafeErrorPage(500, "Whoops! Internal Error", rpyNoResource)
 
 def ResourceScript(path, registry):
     """
@@ -75,7 +75,7 @@ def ResourceTemplate(path, registry):
     from quixote import ptl_compile
 
     glob = {'__file__': _coerceToFilesystemEncoding("", path),
-            'resource': resource.ErrorPage(500, "Whoops! Internal Error",
+            'resource': resource._UnsafeErrorPage(500, "Whoops! Internal Error",
                                            rpyNoResource),
             'registry': registry}
 
@@ -129,10 +129,10 @@ class ResourceScriptDirectory(resource.R
             return ResourceScriptDirectory(fn, self.registry)
         if os.path.exists(fn):
             return ResourceScript(fn, self.registry)
-        return resource.NoResource()
+        return resource._UnsafeNoResource()
 
     def render(self, request):
-        return resource.NoResource().render(request)
+        return resource._UnsafeNoResource().render(request)
 
 
 
Index: Twisted-19.10.0/src/twisted/web/server.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/server.py
+++ Twisted-19.10.0/src/twisted/web/server.py
@@ -343,11 +343,12 @@ class Request(Copyable, http.Request, co
                          'allowed': ', '.join(
                             [nativeString(x) for x in allowedMethods])
                      })
-                epage = resource.ErrorPage(http.NOT_ALLOWED,
-                                           "Method Not Allowed", s)
+                epage = resource._UnsafeErrorPage(
+                    http.NOT_ALLOWED, "Method Not Allowed", s
+                )
                 body = epage.render(self)
             else:
-                epage = resource.ErrorPage(
+                epage = resource._UnsafeErrorPage(
                     http.NOT_IMPLEMENTED, "Huh?",
                     "I don't know how to treat a %s request." %
                     (escape(self.method.decode("charmap")),))
@@ -357,9 +358,10 @@ class Request(Copyable, http.Request, co
         if body is NOT_DONE_YET:
             return
         if not isinstance(body, bytes):
-            body = resource.ErrorPage(
+            body = resource._UnsafeErrorPage(
                 http.INTERNAL_SERVER_ERROR,
                 "Request did not return bytes",
+                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
                 "Request: " + util._PRE(reflect.safe_repr(self)) + "<br />" +
                 "Resource: " + util._PRE(reflect.safe_repr(resrc)) + "<br />" +
                 "Value: " + util._PRE(reflect.safe_repr(body))).render(self)
@@ -624,7 +626,8 @@ class GzipEncoderFactory(object):
 
     @since: 12.3
     """
-    _gzipCheckRegex = re.compile(br'(:?^|[\s,])gzip(:?$|[\s,])')
+    _gzipCheckRegex = re.compile(rb"(:?^|[\s,])gzip(:?$|[\s,])")
+
     compressLevel = 9
 
     def encoderForRequest(self, request):
Index: Twisted-19.10.0/src/twisted/web/static.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/static.py
+++ Twisted-19.10.0/src/twisted/web/static.py
@@ -39,7 +39,7 @@ if _PY3:
 else:
     from urllib import quote, unquote
 
-dangerousPathError = resource.NoResource("Invalid request URL.")
+dangerousPathError = resource._UnsafeNoResource("Invalid request URL.")
 
 def isDangerous(path):
     return path == b'..' or b'/' in path or networkString(os.sep) in path
@@ -268,8 +268,8 @@ class File(resource.Resource, filepath.F
         """
         self.ignoredExts.append(ext)
 
-    childNotFound = resource.NoResource("File not found.")
-    forbidden = resource.ForbiddenResource()
+    childNotFound = resource._UnsafeNoResource("File not found.")
+    forbidden = resource._UnsafeForbiddenResource()
 
 
     def directoryListing(self):
Index: Twisted-19.10.0/docs/web/howto/web-in-60/error-handling.rst
===================================================================
--- Twisted-19.10.0.orig/docs/web/howto/web-in-60/error-handling.rst
+++ Twisted-19.10.0/docs/web/howto/web-in-60/error-handling.rst
@@ -54,10 +54,6 @@ the :doc:`previous one <dynamic-dispatch
 interpreted as a year; the difference is that this time we'll handle requests
 which don't conform to that pattern by returning the not found response:
 
-
-
-
-
 .. code-block:: python
 
 
@@ -66,7 +62,7 @@ which don't conform to that pattern by r
             try:
                 year = int(name)
             except ValueError:
-                return NoResource()
+                return notFound()
             else:
                 return YearPage(year)
 
@@ -88,7 +84,7 @@ complete code for this example:
     from twisted.web.server import Site
     from twisted.web.resource import Resource
     from twisted.internet import reactor, endpoints
-    from twisted.web.resource import NoResource
+    from twisted.web.pages import notFound
 
     from calendar import calendar
 
@@ -100,14 +96,14 @@ complete code for this example:
         def render_GET(self, request):
             cal = calendar(self.year)
             return (b"<!DOCTYPE html><html><head><meta charset='utf-8'>"
-                    b"<title></title></head><body><pre>" + cal.encode('utf-8') + "</pre>")
+                    b"<title></title></head><body><pre>" + cal.encode('utf-8') + b"</pre>")
 
     class Calendar(Resource):
         def getChild(self, name, request):
             try:
                 year = int(name)
             except ValueError:
-                return NoResource()
+                return notFound()
             else:
                 return YearPage(year)
 
Index: Twisted-19.10.0/src/twisted/web/test/test_vhost.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/test/test_vhost.py
+++ Twisted-19.10.0/src/twisted/web/test/test_vhost.py
@@ -68,7 +68,7 @@ class NameVirtualHostTests(TestCase):
         """
         virtualHostResource = NameVirtualHost()
         virtualHostResource.default = Data(b"correct result", "")
-        request = DummyRequest([''])
+        request = DummyRequest([b''])
         self.assertEqual(
             virtualHostResource.render(request), b"correct result")
 
@@ -80,7 +80,7 @@ class NameVirtualHostTests(TestCase):
         header in the request.
         """
         virtualHostResource = NameVirtualHost()
-        request = DummyRequest([''])
+        request = DummyRequest([b''])
         d = _render(virtualHostResource, request)
         def cbRendered(ignored):
             self.assertEqual(request.responseCode, NOT_FOUND)
@@ -148,6 +148,19 @@ class NameVirtualHostTests(TestCase):
         return d
 
 
+    async def test_renderWithHTMLHost(self):
+        """
+        L{NameVirtualHost.render} doesn't echo unescaped HTML when present in
+        the I{Host} header.
+        """
+        virtualHostResource = NameVirtualHost()
+        request = DummyRequest([b""])
+        request.requestHeaders.addRawHeader(b"host", b"<b>example</b>.com")
+
+        await _render(virtualHostResource, request)
+
+        self.assertNotIn(b"<b>", b"".join(request.written))
+
     def test_getChild(self):
         """
         L{NameVirtualHost.getChild} returns correct I{Resource} based off
Index: Twisted-19.10.0/src/twisted/web/newsfragments/11716.bugfix
===================================================================
--- /dev/null
+++ Twisted-19.10.0/src/twisted/web/newsfragments/11716.bugfix
@@ -0,0 +1 @@
+twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (CVE-2022-39348, GHSA-vg46-2rrj-3647).
Index: Twisted-19.10.0/src/twisted/web/vhost.py
===================================================================
--- Twisted-19.10.0.orig/src/twisted/web/vhost.py
+++ Twisted-19.10.0/src/twisted/web/vhost.py
@@ -10,7 +10,7 @@ from __future__ import division, absolut
 
 # Twisted Imports
 from twisted.python import roots
-from twisted.web import resource
+from twisted.web import pages, resource
 
 
 class VirtualHostCollection(roots.Homogenous):
@@ -78,12 +78,12 @@ class NameVirtualHost(resource.Resource)
         """(Internal) Get the appropriate resource for the given host.
         """
         hostHeader = request.getHeader(b'host')
-        if hostHeader == None:
-            return self.default or resource.NoResource()
+        if hostHeader is None:
+            return self.default or pages.notFound()
         else:
             host = hostHeader.lower().split(b':', 1)[0]
         return (self.hosts.get(host, self.default)
-                or resource.NoResource("host %s not in vhost map" % repr(host)))
+                or pages.notFound("Not Found", "host %s not in vhost map" % repr(host)))
 
     def render(self, request):
         """Implementation of resource.Resource's render method.
openSUSE Build Service is sponsored by