VYPR
Moderate severityNVD Advisory· Published Oct 26, 2022· Updated Nov 3, 2025

Twisted vulnerable to NameVirtualHost Host header injection

CVE-2022-39348

Description

Twisted is an event-based framework for internet applications. Started with version 0.9.4, when the host header does not match a configured host twisted.web.vhost.NameVirtualHost will return a NoResource resource which renders the Host header unescaped into the 404 response allowing HTML and script injection. In practice this should be very difficult to exploit as being able to modify the Host header of a normal HTTP request implies that one is already in a privileged position. This issue was fixed in version 22.10.0rc1. There are no known workarounds.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
TwistedPyPI
>= 0.9.4, < 22.10.0rc122.10.0rc1

Affected products

1

Patches

2
f2f5e81c03f1

Merge pull request from GHSA-vg46-2rrj-3647

https://github.com/twisted/twistedAdi RoibanOct 26, 2022via ghsa
16 files changed · +416 68
  • docs/web/howto/web-in-60/error-handling.rst+7 19 modified
    @@ -32,32 +32,20 @@ As in the previous examples, we'll start with :py:class:`Site <twisted.web.serve
     
     
     
    -Next, we'll add one more import. :py:class:`NoResource <twisted.web.resource.NoResource>` is one of the pre-defined error
    +Next, we'll add one more import. :py:class:`notFound <twisted.web.pages.notFound>` is one of the pre-defined error
     resources provided by Twisted Web. It generates the necessary 404 response code
    -and renders a simple html page telling the client there is no such resource.
    -
    -
    -
    -
    +and renders a simple HTML page telling the client there is no such resource.
     
     .. code-block:: python
     
    -
    -    from twisted.web.resource import NoResource
    -
    -
    -
    +    from twisted.web.pages import notFound
     
     Next, we'll define a custom resource which does some dynamic URL
     dispatch. This example is going to be just like
     the :doc:`previous one <dynamic-dispatch>` , where the path segment is
     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 +54,7 @@ which don't conform to that pattern by returning the not found response:
                 try:
                     year = int(name)
                 except ValueError:
    -                return NoResource()
    +                return notFound()
                 else:
                     return YearPage(year)
     
    @@ -88,7 +76,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 +88,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)
     
    
  • src/twisted/web/_auth/wrapper.py+4 4 modified
    @@ -21,7 +21,7 @@
     from twisted.logger import Logger
     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
     
     
     @implementer(IResource)
    @@ -52,7 +52,7 @@ def generateWWWAuthenticate(scheme, challenge):
                 return b" ".join([scheme, b", ".join(lst)])
     
             def quoteString(s):
    -            return b'"' + s.replace(b"\\", br"\\").replace(b'"', br"\"") + b'"'
    +            return b'"' + s.replace(b"\\", rb"\\").replace(b'"', rb"\"") + b'"'
     
             request.setResponseCode(401)
             for fact in self._credentialFactories:
    @@ -125,7 +125,7 @@ def _authorizedResource(self, request):
                 return UnauthorizedResource(self._credentialFactories)
             except BaseException:
                 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))
     
    @@ -213,7 +213,7 @@ def _loginFailed(self, result):
                     "unexpected error",
                     failure=result,
                 )
    -            return ErrorPage(500, None, None)
    +            return _UnsafeErrorPage(500, "Internal Error", "")
     
         def _selectParseHeader(self, header):
             """
    
  • src/twisted/web/distrib.py+4 3 modified
    @@ -124,9 +124,10 @@ def failed(self, failure):
             # XXX: Argh. FIXME.
             failure = str(failure)
             self.request.write(
    -            resource.ErrorPage(
    +            resource._UnsafeErrorPage(
                     http.INTERNAL_SERVER_ERROR,
                     "Server Connection Lost",
    +                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
                     "Connection to distributed server lost:" + util._PRE(failure),
                 ).render(self.request)
             )
    @@ -376,7 +377,7 @@ def getChild(self, name, request):
                     pw_shell,
                 ) = self._pwd.getpwnam(username.decode(sys.getfilesystemencoding()))
             except KeyError:
    -            return resource.NoResource()
    +            return resource._UnsafeNoResource()
             if sub:
                 twistdsock = os.path.join(pw_dir, self.userSocketName)
                 rs = ResourceSubscription("unix", twistdsock)
    @@ -385,5 +386,5 @@ def getChild(self, name, request):
             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)
    
  • src/twisted/web/newsfragments/11716.bugfix+1 0 added
    @@ -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).
    
  • src/twisted/web/newsfragments/11716.feature+1 0 added
    @@ -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.
    
  • src/twisted/web/newsfragments/11716.removal+1 0 added
    @@ -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.
    
  • src/twisted/web/pages.py+134 0 added
    @@ -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)
    
  • src/twisted/web/resource.py+60 15 modified
    @@ -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.
     """
     
     
    @@ -21,8 +23,11 @@
     
     from zope.interface import Attribute, Interface, implementer
     
    +from incremental import Version
    +
     from twisted.python.compat import nativeString
     from twisted.python.components import proxyForInterface
    +from twisted.python.deprecate import deprecatedModuleAttribute
     from twisted.python.reflect import prefixedMethodNames
     from twisted.web._responses import FORBIDDEN, NOT_FOUND
     from twisted.web.error import UnsupportedMethod
    @@ -180,7 +185,7 @@ def getChild(self, path, request):
             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):
             """
    @@ -287,20 +292,25 @@ def _computeAllowedMethods(resource):
         return allowedMethods
     
     
    -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}
    @@ -343,24 +353,59 @@ def getChild(self, chnam, request):
             return self
     
     
    -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):
    
  • src/twisted/web/script.py+9 5 modified
    @@ -49,7 +49,7 @@ 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):
    @@ -81,7 +81,9 @@ def ResourceTemplate(path, registry):
     
         glob = {
             "__file__": _coerceToFilesystemEncoding("", path),
    -        "resource": resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource),
    +        "resource": resource._UnsafeErrorPage(
    +            500, "Whoops! Internal Error", rpyNoResource
    +        ),
             "registry": registry,
         }
     
    @@ -133,10 +135,10 @@ def getChild(self, path, request):
                 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)
     
     
     class PythonScript(resource.Resource):
    @@ -178,7 +180,9 @@ def render(self, request):
             except OSError as e:
                 if e.errno == 2:  # file not found
                     request.setResponseCode(http.NOT_FOUND)
    -                request.write(resource.NoResource("File not found.").render(request))
    +                request.write(
    +                    resource._UnsafeNoResource("File not found.").render(request)
    +                )
             except BaseException:
                 io = StringIO()
                 traceback.print_exc(file=io)
    
  • src/twisted/web/server.py+7 4 modified
    @@ -335,10 +335,12 @@ def render(self, resrc):
                             "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."
    @@ -350,10 +352,11 @@ def render(self, resrc):
             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",
                     "Request: "
    +                # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
                     + util._PRE(reflect.safe_repr(self))
                     + "<br />"
                     + "Resource: "
    @@ -607,7 +610,7 @@ class GzipEncoderFactory:
         @since: 12.3
         """
     
    -    _gzipCheckRegex = re.compile(br"(:?^|[\s,])gzip(:?$|[\s,])")
    +    _gzipCheckRegex = re.compile(rb"(:?^|[\s,])gzip(:?$|[\s,])")
         compressLevel = 9
     
         def encoderForRequest(self, request):
    
  • src/twisted/web/static.py+3 3 modified
    @@ -31,7 +31,7 @@
     from twisted.web import http, resource, server
     from twisted.web.util import redirectTo
     
    -dangerousPathError = resource.NoResource("Invalid request URL.")
    +dangerousPathError = resource._UnsafeNoResource("Invalid request URL.")
     
     
     def isDangerous(path):
    @@ -255,8 +255,8 @@ def ignoreExt(self, ext):
             """
             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):
             """
    
  • src/twisted/web/_template_util.py+3 3 modified
    @@ -1034,9 +1034,9 @@ class _TagFactory:
         """
         A factory for L{Tag} objects; the implementation of the L{tags} object.
     
    -    This allows for the syntactic convenience of C{from twisted.web.html import
    -    tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML
    -    tag.
    +    This allows for the syntactic convenience of C{from twisted.web.template
    +    import tags; tags.a(href="linked-page.html")}, where 'a' can be basically
    +    any HTML tag.
     
         The class is not exposed publicly because you only ever need one of these,
         and we already made it for you.
    
  • src/twisted/web/test/test_pages.py+113 0 added
    @@ -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>"
    +            ),
    +        )
    
  • src/twisted/web/test/test_resource.py+47 4 modified
    @@ -11,24 +11,67 @@
     from twisted.web.resource import (
         FORBIDDEN,
         NOT_FOUND,
    -    ErrorPage,
    -    ForbiddenResource,
    -    NoResource,
         Resource,
    +    _UnsafeErrorPage as ErrorPage,
    +    _UnsafeForbiddenResource as ForbiddenResource,
    +    _UnsafeNoResource as NoResource,
         getChildForRequest,
     )
     from twisted.web.test.requesthelper import DummyRequest
     
     
     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
    
  • src/twisted/web/test/test_vhost.py+16 3 modified
    @@ -66,7 +66,7 @@ def test_renderWithoutHost(self):
             """
             virtualHostResource = NameVirtualHost()
             virtualHostResource.default = Data(b"correct result", "")
    -        request = DummyRequest([""])
    +        request = DummyRequest([b""])
             self.assertEqual(virtualHostResource.render(request), b"correct result")
     
         def test_renderWithoutHostNoDefault(self):
    @@ -76,7 +76,7 @@ def test_renderWithoutHostNoDefault(self):
             header in the request.
             """
             virtualHostResource = NameVirtualHost()
    -        request = DummyRequest([""])
    +        request = DummyRequest([b""])
             d = _render(virtualHostResource, request)
     
             def cbRendered(ignored):
    @@ -140,7 +140,7 @@ def test_renderWithUnknownHostNoDefault(self):
             matching the value of the I{Host} header in the request.
             """
             virtualHostResource = NameVirtualHost()
    -        request = DummyRequest([""])
    +        request = DummyRequest([b""])
             request.requestHeaders.addRawHeader(b"host", b"example.com")
             d = _render(virtualHostResource, request)
     
    @@ -150,6 +150,19 @@ def cbRendered(ignored):
             d.addCallback(cbRendered)
             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
    
  • src/twisted/web/vhost.py+6 5 modified
    @@ -9,7 +9,7 @@
     
     # Twisted Imports
     from twisted.python import roots
    -from twisted.web import resource
    +from twisted.web import pages, resource
     
     
     class VirtualHostCollection(roots.Homogenous):
    @@ -77,12 +77,13 @@ def removeHost(self, name):
         def _getResourceForRequest(self, request):
             """(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)
    +        return self.hosts.get(host, self.default) or pages.notFound(
    +            "Not Found",
    +            f"host {host.decode('ascii', 'replace')!r} not in vhost map",
             )
     
         def render(self, request):
    
f49041bb6779

vhost bugfix...

https://github.com/twisted/twistedglyphJul 23, 2001via ghsa
1 file changed · +1 1
  • twisted/web/vhost.py+1 1 modified
    @@ -34,7 +34,7 @@ def _getResourceForRequest(self, request):
             """(Internal) Get the appropriate resource for the given host.
             """
             host = string.lower(request.getHeader('host'))
    -        return self.hosts.get(host, error.NoResource())
    +        return self.hosts.get(host, error.NoResource("host %s not in vhost map" % repr(host)))
             
         def render(self, request):
             """Implementation of resource.Resource's render method.
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.