Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Leap:15.5:Update
python3-Twisted.34928
CVE-2022-39348-do-not-echo-host-header.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File CVE-2022-39348-do-not-echo-host-header.patch of Package python3-Twisted.34928
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 --- docs/web/howto/web-in-60/error-handling.rst | 26 +---- src/twisted/web/_auth/wrapper.py | 8 - src/twisted/web/_template_util.py | 6 - src/twisted/web/distrib.py | 7 - src/twisted/web/newsfragments/11716.bugfix | 1 src/twisted/web/newsfragments/11716.feature | 1 src/twisted/web/newsfragments/11716.removal | 1 src/twisted/web/pages.py | 134 ++++++++++++++++++++++++++++ src/twisted/web/resource.py | 75 ++++++++++++--- src/twisted/web/script.py | 14 +- src/twisted/web/server.py | 11 +- src/twisted/web/static.py | 6 - src/twisted/web/test/test_pages.py | 113 +++++++++++++++++++++++ src/twisted/web/test/test_resource.py | 51 +++++++++- src/twisted/web/test/test_vhost.py | 19 +++ src/twisted/web/vhost.py | 11 +- 16 files changed, 416 insertions(+), 68 deletions(-) create mode 100644 src/twisted/web/newsfragments/11716.feature create mode 100644 src/twisted/web/newsfragments/11716.removal --- a/docs/web/howto/web-in-60/error-handling.rst +++ b/docs/web/howto/web-in-60/error-handling.rst @@ -32,21 +32,13 @@ As in the previous examples, we'll start -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 @@ -54,10 +46,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 +54,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 +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) --- a/src/twisted/web/_auth/wrapper.py +++ b/src/twisted/web/_auth/wrapper.py @@ -21,7 +21,7 @@ from twisted.cred.credentials import Ano 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 @@ class UnauthorizedResource: 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 @@ class HTTPAuthSessionWrapper: 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 @@ class HTTPAuthSessionWrapper: "unexpected error", failure=result, ) - return ErrorPage(500, None, None) + return _UnsafeErrorPage(500, "Internal Error", "") def _selectParseHeader(self, header): """ --- a/src/twisted/web/_template_util.py +++ b/src/twisted/web/_template_util.py @@ -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. --- a/src/twisted/web/distrib.py +++ b/src/twisted/web/distrib.py @@ -127,9 +127,10 @@ class Issue: # 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) ) @@ -377,7 +378,7 @@ class UserDirectory(resource.Resource): 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) @@ -386,5 +387,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) --- /dev/null +++ b/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). --- /dev/null +++ b/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. --- /dev/null +++ b/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. --- /dev/null +++ b/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) --- a/src/twisted/web/resource.py +++ b/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. """ @@ -21,8 +23,11 @@ import warnings 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 @@ -178,7 +183,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): """ @@ -286,20 +291,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} @@ -342,24 +352,59 @@ class ErrorPage(Resource): 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): --- a/src/twisted/web/script.py +++ b/src/twisted/web/script.py @@ -49,7 +49,7 @@ class CacheScanner: 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 @@ 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) class PythonScript(resource.Resource): @@ -178,7 +180,9 @@ class PythonScript(resource.Resource): 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) --- a/src/twisted/web/server.py +++ b/src/twisted/web/server.py @@ -334,10 +334,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." @@ -349,10 +351,11 @@ 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", "Request: " + # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input. + util._PRE(reflect.safe_repr(self)) + "<br />" + "Resource: " @@ -606,7 +609,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): --- a/src/twisted/web/static.py +++ b/src/twisted/web/static.py @@ -31,7 +31,7 @@ from twisted.python.util import Insensit 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 @@ 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): """ --- /dev/null +++ b/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 & B</title></head>" + b"<body><h1>A & B</h1><p><script>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>" + ), + ) --- a/src/twisted/web/test/test_resource.py +++ b/src/twisted/web/test/test_resource.py @@ -11,10 +11,10 @@ from twisted.web.http_headers import Hea 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 @@ -22,13 +22,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 --- a/src/twisted/web/test/test_vhost.py +++ b/src/twisted/web/test/test_vhost.py @@ -66,7 +66,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") def test_renderWithoutHostNoDefault(self): @@ -76,7 +76,7 @@ class NameVirtualHostTests(TestCase): header in the request. """ virtualHostResource = NameVirtualHost() - request = DummyRequest([""]) + request = DummyRequest([b""]) d = _render(virtualHostResource, request) def cbRendered(ignored): @@ -140,7 +140,7 @@ class NameVirtualHostTests(TestCase): 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 @@ class NameVirtualHostTests(TestCase): 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 --- a/src/twisted/web/vhost.py +++ b/src/twisted/web/vhost.py @@ -9,7 +9,7 @@ I am a virtual hosts implementation. # 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 @@ class NameVirtualHost(resource.Resource) 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):
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor