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.