Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15-SP2:Update
python-Twisted.31485
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 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 & 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>" + ), + ) 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.
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