Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12-SP4:GA
python-Twisted
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
ndex: Twisted-19.10.0/twisted/web/newsfragments/11716.feature =================================================================== Index: Twisted-15.2.1/twisted/web/newsfragments/11716.feature =================================================================== --- /dev/null +++ Twisted-15.2.1/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-15.2.1/twisted/web/newsfragments/11716.removal =================================================================== --- /dev/null +++ Twisted-15.2.1/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-15.2.1/twisted/web/resource.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/resource.py +++ Twisted-15.2.1/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 @@ -20,6 +22,8 @@ from zope.interface import Attribute, In 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.python.versions import Version from twisted.web._responses import FORBIDDEN, NOT_FOUND from twisted.web.error import UnsupportedMethod @@ -101,7 +105,7 @@ def getChildForRequest(resource, request @implementer(IResource) -class Resource: +class Resource(object): """ Define a web-accessible resource. @@ -179,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): @@ -277,20 +281,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} @@ -335,24 +344,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", 20, 10, 0), + "Use twisted.web.pages.errorPage instead, which properly escapes HTML.", + __name__, + "ErrorPage", +) + +deprecatedModuleAttribute( + Version("Twisted", 20, 10, 0), + "Use twisted.web.pages.notFound instead, which properly escapes HTML.", + __name__, + "NoResource", +) + +deprecatedModuleAttribute( + Version("Twisted", 20, 10, 0), + "Use twisted.web.pages.forbidden instead, which properly escapes HTML.", + __name__, + "ForbiddenResource", +) class _IEncodingResource(Interface): Index: Twisted-15.2.1/twisted/web/test/test_resource.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/test/test_resource.py +++ Twisted-15.2.1/twisted/web/test/test_resource.py @@ -9,20 +9,66 @@ from twisted.trial.unittest import TestC 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.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 Index: Twisted-15.2.1/twisted/web/pages.py =================================================================== --- /dev/null +++ Twisted-15.2.1/twisted/web/pages.py @@ -0,0 +1,126 @@ +# -*- 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 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, brief, detail): + super(_ErrorPage, self).__init__() + self._code = code + self._brief = brief + self._detail = detail + + def render(self, request): + """ + 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, + tags.html( + tags.head(tags.title(str(self._code) + " - " + self._brief)), + tags.body(tags.h1(self._brief), tags.p(self._detail)), + ) + ) + + def getChild(self, path, request): + """ + 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, brief, detail): + """ + 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 = "No Such Resource", + message = "Sorry. No luck finding that resource.", + ): + """ + 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 = "Forbidden Resource", message = "Sorry, resource is forbidden." + ): + """ + 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-15.2.1/twisted/web/test/test_pages.py =================================================================== --- /dev/null +++ Twisted-15.2.1/twisted/web/test/test_pages.py @@ -0,0 +1,108 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Test L{twisted.web.pages} +""" + +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): + """ + 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([""]) + resource.render(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, code, body): + self.assertEqual(request.responseCode, code) + self.assertEqual( + request.responseHeaders, + Headers({}), + ) + self.assertEqual( + # Decode to str because unittest somehow still doesn't diff bytes + # without truncating them in 2022. + "".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, + ( + "<!DOCTYPE html>\n" + "<html><head><title>400 - A & B</title></head>" + "<body><h1>A & B</h1><p><script>alert('oops!')" + "</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("name", DummyRequest([""])), + page, + ) + + def test_notFoundDefaults(self): + """ + The default arguments to L{twisted.web.pages.notFound} produce + a reasonable error page. + """ + self.assertResponse( + _render(notFound()), + 404, + ( + "<!DOCTYPE html>\n" + "<html><head><title>404 - No Such Resource</title></head>" + "<body><h1>No Such Resource</h1>" + "<p>Sorry. No luck finding that resource.</p>" + "</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, + ( + "<!DOCTYPE html>\n" + "<html><head><title>403 - Forbidden Resource</title></head>" + "<body><h1>Forbidden Resource</h1>" + "<p>Sorry, resource is forbidden.</p>" + "</body></html>" + ), + ) Index: Twisted-15.2.1/twisted/web/_auth/wrapper.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/_auth/wrapper.py +++ Twisted-15.2.1/twisted/web/_auth/wrapper.py @@ -17,7 +17,7 @@ from zope.interface import implements from twisted.python import log from twisted.python.components import proxyForInterface -from twisted.web.resource import IResource, ErrorPage +from twisted.web.resource import IResource, _UnsafeErrorPage from twisted.web import util from twisted.cred import error from twisted.cred.credentials import Anonymous @@ -116,7 +116,7 @@ class HTTPAuthSessionWrapper(object): return UnauthorizedResource(self._credentialFactories) except: log.err(None, "Unexpected failure from credentials factory") - return ErrorPage(500, None, None) + return _UnsafeErrorPage(500, "Internal Error", "") else: return util.DeferredResource(self._login(credentials)) @@ -205,7 +205,7 @@ class HTTPAuthSessionWrapper(object): result, "HTTPAuthSessionWrapper.getChildWithDefault encountered " "unexpected error") - return ErrorPage(500, None, None) + return _UnsafeErrorPage(500, "Internal Error", "") def _selectParseHeader(self, header): Index: Twisted-15.2.1/twisted/web/distrib.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/distrib.py +++ Twisted-15.2.1/twisted/web/distrib.py @@ -121,11 +121,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:" + - html.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:" + html.PRE(failure) + ).render(self.request)) self.request.finish() log.msg(failure) @@ -360,7 +361,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) @@ -369,5 +370,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-15.2.1/twisted/web/script.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/script.py +++ Twisted-15.2.1/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} @@ -128,10 +128,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-15.2.1/twisted/web/server.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/server.py +++ Twisted-15.2.1/twisted/web/server.py @@ -274,11 +274,12 @@ class Request(Copyable, http.Request, co 'plural': ((len(allowedMethods) > 1) and 's') or '', 'allowed': ', '.join(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")),)) @@ -288,9 +289,10 @@ class Request(Copyable, http.Request, co if body == 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: " + html.PRE(reflect.safe_repr(self)) + "<br />" + "Resource: " + html.PRE(reflect.safe_repr(resrc)) + "<br />" + "Value: " + html.PRE(reflect.safe_repr(body))).render(self) Index: Twisted-15.2.1/twisted/web/static.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/static.py +++ Twisted-15.2.1/twisted/web/static.py @@ -36,7 +36,7 @@ else: from urllib import quote, unquote from cgi import escape -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 @@ -233,8 +233,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): return DirectoryLister(self.path, Index: Twisted-15.2.1/twisted/web/test/test_vhost.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/test/test_vhost.py +++ Twisted-15.2.1/twisted/web/test/test_vhost.py @@ -103,3 +103,19 @@ class NameVirtualHostTests(TestCase): self.assertEqual(request.responseCode, NOT_FOUND) d.addCallback(cbRendered) return d + + + def test_renderWithHTMLHost(self): + """ + L{NameVirtualHost.render} doesn't echo unescaped HTML when present in + the I{Host} header. + """ + virtualHostResource = NameVirtualHost() + request = DummyRequest(['']) + request.headers['host'] = '<b>example.com</b>' + d = _render(virtualHostResource, request) + def cbRendered(ignored): + self.assertEqual(request.responseCode, NOT_FOUND) + self.assertFalse('<b>' in ''.join(request.written)) + d.addCallback(cbRendered) + return d Index: Twisted-15.2.1/twisted/web/newsfragments/11716.bugfix =================================================================== --- /dev/null +++ Twisted-15.2.1/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-15.2.1/twisted/web/vhost.py =================================================================== --- Twisted-15.2.1.orig/twisted/web/vhost.py +++ Twisted-15.2.1/twisted/web/vhost.py @@ -8,7 +8,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): @@ -76,12 +76,12 @@ class NameVirtualHost(resource.Resource) """(Internal) Get the appropriate resource for the given host. """ hostHeader = request.getHeader('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(':', 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