File CVE-2022-39348-do-not-echo-host-header.patch of Package python-Twisted.26705
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
diff --git a/src/twisted/web/newsfragments/11716.feature b/src/twisted/web/newsfragments/11716.feature
new file mode 100644
index 00000000000..5693458b403
--- /dev/null
+++ b/src/twisted/web/newsfragments/11716.feature
@@ -0,0 +1 @@
+The twisted.web.pages.ErrorPage, NotFoundPage, and ForbiddenPage IResource implementations provide HTML error pages rendered safely using twisted.web.template.
diff --git a/src/twisted/web/newsfragments/11716.removal b/src/twisted/web/newsfragments/11716.removal
new file mode 100644
index 00000000000..f4d2b36f415
--- /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.
diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py
index 5e6bd83f908..93c780740fc 100644
--- 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 @@
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
@@ -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.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,26 +352,61 @@ def getChild(self, chnam, request):
return self
-class NoResource(ErrorPage):
+class _UnsafeNoResource(_UnsafeErrorPage):
"""
- L{NoResource} is a specialization of L{ErrorPage} which returns the HTTP
- response code I{NOT FOUND}.
+ L{_UnsafeNoResource}, publicly available via the deprecated alias
+ C{NoResource}, is a specialization of L{_UnsafeErrorPage} which
+ returns the HTTP response code I{NOT FOUND}.
+
+ Deprecated in Twisted NEXT because it permits HTML injection; use
+ L{twisted.pages.NotFoundPage} instead.
"""
def __init__(self, message="Sorry. No luck finding that resource."):
ErrorPage.__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.pages.ForbiddenPage} instead.
"""
def __init__(self, message="Sorry, resource is forbidden."):
ErrorPage.__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.pages.ErrorPage instead, which properly escapes HTML.",
+ __name__,
+ "ErrorPage",
+)
+
+deprecatedModuleAttribute(
+ Version("Twisted", "NEXT", 0, 0),
+ "Use twisted.pages.NotFoundPage instead, which properly escapes HTML.",
+ __name__,
+ "NoResource",
+)
+
+deprecatedModuleAttribute(
+ Version("Twisted", "NEXT", 0, 0),
+ "Use twisted.pages.ForbiddenPage instead, which properly escapes HTML.",
+ __name__,
+ "ForbiddenResource",
+)
+
+
class _IEncodingResource(Interface):
"""
A resource which knows about L{_IRequestEncoderFactory}.
diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py
index bd2f90887da..3e83d0efdc2 100644
--- a/src/twisted/web/test/test_resource.py
+++ b/src/twisted/web/test/test_resource.py
@@ -11,10 +11,10 @@
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 @@
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.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.pages.NotFoundPage", 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.pages.ForbiddenPage", warning["message"])
+
def test_getChild(self):
"""
The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
From 5ce023c4d735a895a03f2eb4a622a2322b8990ec Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Thu, 20 Oct 2022 23:38:19 -0700
Subject: [PATCH 02/12] Implement twisted.web.pages
---
src/twisted/web/_template_util.py | 6 +-
src/twisted/web/newsfragments/11716.feature | 2 +-
src/twisted/web/pages.py | 108 ++++++++++++++++++++
src/twisted/web/resource.py | 10 +-
src/twisted/web/test/test_pages.py | 106 +++++++++++++++++++
src/twisted/web/test/test_resource.py | 4 +-
6 files changed, 225 insertions(+), 11 deletions(-)
create mode 100644 src/twisted/web/pages.py
create mode 100644 src/twisted/web/test/test_pages.py
diff --git a/src/twisted/web/_template_util.py b/src/twisted/web/_template_util.py
index bd081bd54ab..38ebbed1d5b 100644
--- 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.
diff --git a/src/twisted/web/newsfragments/11716.feature b/src/twisted/web/newsfragments/11716.feature
index 5693458b403..e8ba00b7ef2 100644
--- a/src/twisted/web/newsfragments/11716.feature
+++ b/src/twisted/web/newsfragments/11716.feature
@@ -1 +1 @@
-The twisted.web.pages.ErrorPage, NotFoundPage, and ForbiddenPage IResource implementations provide HTML error pages rendered safely using twisted.web.template.
+The twisted.web.pages.ErrorPage, notFound, and forbidden IResource implementations provide HTML error pages safely rendered using twisted.web.template.
diff --git a/src/twisted/web/pages.py b/src/twisted/web/pages.py
new file mode 100644
index 00000000000..8f37b3e45a8
--- /dev/null
+++ b/src/twisted/web/pages.py
@@ -0,0 +1,108 @@
+# -*- 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 IRequest
+from twisted.web.resource import 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 a body consisting of HTML containing
+ some descriptive text. This is useful for rendering simple error pages.
+
+ @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:
+ """
+ @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.
+ """
+ super().__init__()
+ self._code: int = code
+ self._brief: str = brief
+ self._detail: str = detail
+
+ def render(self, request: IRequest) -> None:
+ """
+ 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")
+ renderElement(
+ request,
+ 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 notFound(
+ brief: str = "No Such Resource",
+ message: str = "Sorry. No luck finding that resource.",
+) -> ErrorPage:
+ """
+ Generate an L{ErrorPage} with a 404 Not Found status code.
+
+ @param brief: A short string displayed as the page title.
+
+ @param brief: A longer string displayed in the page body.
+
+ @returns: An L{ErrorPage}
+ """
+ return ErrorPage(http.NOT_FOUND, brief, message)
+
+
+def forbidden(
+ brief: str = "Forbidden Resource", message: str = "Sorry, resource is forbidden."
+) -> ErrorPage:
+ """
+ Generate an L{ErrorPage} with a 403 Forbidden status code.
+
+ @param brief: A short string displayed as the page title.
+
+ @param brief: A longer string displayed in the page body.
+
+ @returns: An L{ErrorPage}
+ """
+ return ErrorPage(http.FORBIDDEN, brief, message)
diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py
index 93c780740fc..09fc74a89fd 100644
--- a/src/twisted/web/resource.py
+++ b/src/twisted/web/resource.py
@@ -183,7 +183,7 @@ def getChild(self, path, request):
Parameters and return value have the same meaning and requirements as
those defined by L{IResource.getChildWithDefault}.
"""
- return NoResource("No such child resource.")
+ return _UnsafeNoResource()
def getChildWithDefault(self, path, request):
"""
@@ -359,7 +359,7 @@ class _UnsafeNoResource(_UnsafeErrorPage):
returns the HTTP response code I{NOT FOUND}.
Deprecated in Twisted NEXT because it permits HTML injection; use
- L{twisted.pages.NotFoundPage} instead.
+ L{twisted.pages.notFound} instead.
"""
def __init__(self, message="Sorry. No luck finding that resource."):
@@ -373,7 +373,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage):
returns the I{FORBIDDEN} HTTP response code.
Deprecated in Twisted NEXT because it permits HTML injection; use
- L{twisted.pages.ForbiddenPage} instead.
+ L{twisted.pages.forbidden} instead.
"""
def __init__(self, message="Sorry, resource is forbidden."):
@@ -394,14 +394,14 @@ def __init__(self, message="Sorry, resource is forbidden."):
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.pages.NotFoundPage instead, which properly escapes HTML.",
+ "Use twisted.pages.notFound instead, which properly escapes HTML.",
__name__,
"NoResource",
)
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.pages.ForbiddenPage instead, which properly escapes HTML.",
+ "Use twisted.pages.forbidden instead, which properly escapes HTML.",
__name__,
"ForbiddenResource",
)
diff --git a/src/twisted/web/test/test_pages.py b/src/twisted/web/test/test_pages.py
new file mode 100644
index 00000000000..d83e0eba3e2
--- /dev/null
+++ b/src/twisted/web/test/test_pages.py
@@ -0,0 +1,106 @@
+# 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.pages import ErrorPage, forbidden, notFound
+from twisted.web.test.requesthelper import DummyRequest
+
+
+def _render(resource: ErrorPage) -> 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""])
+ resource.render(request)
+ return request
+
+
+class ErrorPageTests(SynchronousTestCase):
+ """
+ Test L{twisted.web.pages.ErrorPage} and its convencience helpers
+ 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 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>"
+ ),
+ )
diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py
index 3e83d0efdc2..c039704a79d 100644
--- a/src/twisted/web/test/test_resource.py
+++ b/src/twisted/web/test/test_resource.py
@@ -56,7 +56,7 @@ def test_deprecatedNoResource(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.pages.NotFoundPage", warning["message"])
+ self.assertIn("twisted.pages.notFound", warning["message"])
def test_deprecatedForbiddenResource(self):
"""
@@ -70,7 +70,7 @@ def test_deprecatedForbiddenResource(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.pages.ForbiddenPage", warning["message"])
+ self.assertIn("twisted.pages.forbidden", warning["message"])
def test_getChild(self):
"""
From 7c48ed2b6282a49a73d31c7e952e0e115599c83f Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 18:20:39 -0700
Subject: [PATCH 03/12] Update imports to avoid warnings
Use _UnsafeErrorPage, _UnsafeNoResource, etc. symbols instead of the
public aliases that provoke deprecation warnings.
---
src/twisted/web/_auth/wrapper.py | 8 ++++----
src/twisted/web/distrib.py | 7 ++++---
src/twisted/web/script.py | 14 +++++++++-----
src/twisted/web/server.py | 11 +++++++----
src/twisted/web/static.py | 6 +++---
5 files changed, 27 insertions(+), 19 deletions(-)
diff --git a/src/twisted/web/_auth/wrapper.py b/src/twisted/web/_auth/wrapper.py
index 0f71380a4d8..cffdcff66c9 100644
--- a/src/twisted/web/_auth/wrapper.py
+++ b/src/twisted/web/_auth/wrapper.py
@@ -21,7 +21,7 @@
from twisted.logger import Logger
from twisted.python.components import proxyForInterface
from twisted.web import util
-from twisted.web.resource import ErrorPage, IResource
+from twisted.web.resource import IResource, _UnsafeErrorPage
@implementer(IResource)
@@ -52,7 +52,7 @@ def generateWWWAuthenticate(scheme, challenge):
return b" ".join([scheme, b", ".join(lst)])
def quoteString(s):
- return b'"' + s.replace(b"\\", br"\\").replace(b'"', br"\"") + b'"'
+ return b'"' + s.replace(b"\\", rb"\\").replace(b'"', rb"\"") + b'"'
request.setResponseCode(401)
for fact in self._credentialFactories:
@@ -125,7 +125,7 @@ def _authorizedResource(self, request):
return UnauthorizedResource(self._credentialFactories)
except BaseException:
self._log.failure("Unexpected failure from credentials factory")
- return ErrorPage(500, None, None)
+ return _UnsafeErrorPage(500, "Internal Error", "")
else:
return util.DeferredResource(self._login(credentials))
@@ -213,7 +213,7 @@ def _loginFailed(self, result):
"unexpected error",
failure=result,
)
- return ErrorPage(500, None, None)
+ return _UnsafeErrorPage(500, "Internal Error", "")
def _selectParseHeader(self, header):
"""
diff --git a/src/twisted/web/distrib.py b/src/twisted/web/distrib.py
index 56f83fe2792..05665278ed8 100644
--- a/src/twisted/web/distrib.py
+++ b/src/twisted/web/distrib.py
@@ -127,9 +127,10 @@ def failed(self, failure):
# XXX: Argh. FIXME.
failure = str(failure)
self.request.write(
- resource.ErrorPage(
+ resource._UnsafeErrorPage(
http.INTERNAL_SERVER_ERROR,
"Server Connection Lost",
+ # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
"Connection to distributed server lost:" + util._PRE(failure),
).render(self.request)
)
@@ -377,7 +378,7 @@ def getChild(self, name, request):
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 @@ def getChild(self, name, request):
else:
path = os.path.join(pw_dir, self.userDirName)
if not os.path.exists(path):
- return resource.NoResource()
+ return resource._UnsafeNoResource()
return static.File(path)
diff --git a/src/twisted/web/script.py b/src/twisted/web/script.py
index eaf4ab8c8fc..bc4a90f748a 100644
--- a/src/twisted/web/script.py
+++ b/src/twisted/web/script.py
@@ -49,7 +49,7 @@ def recache(self):
self.doCache = 1
-noRsrc = resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource)
+noRsrc = resource._UnsafeErrorPage(500, "Whoops! Internal Error", rpyNoResource)
def ResourceScript(path, registry):
@@ -81,7 +81,9 @@ def ResourceTemplate(path, registry):
glob = {
"__file__": _coerceToFilesystemEncoding("", path),
- "resource": resource.ErrorPage(500, "Whoops! Internal Error", rpyNoResource),
+ "resource": resource._UnsafeErrorPage(
+ 500, "Whoops! Internal Error", rpyNoResource
+ ),
"registry": registry,
}
@@ -133,10 +135,10 @@ def getChild(self, path, request):
return ResourceScriptDirectory(fn, self.registry)
if os.path.exists(fn):
return ResourceScript(fn, self.registry)
- return resource.NoResource()
+ return resource._UnsafeNoResource()
def render(self, request):
- return resource.NoResource().render(request)
+ return resource._UnsafeNoResource().render(request)
class PythonScript(resource.Resource):
@@ -178,7 +180,9 @@ def render(self, request):
except OSError as e:
if e.errno == 2: # file not found
request.setResponseCode(http.NOT_FOUND)
- request.write(resource.NoResource("File not found.").render(request))
+ request.write(
+ resource._UnsafeNoResource("File not found.").render(request)
+ )
except BaseException:
io = StringIO()
traceback.print_exc(file=io)
diff --git a/src/twisted/web/server.py b/src/twisted/web/server.py
index d30156b895a..e8e01ec781b 100644
--- a/src/twisted/web/server.py
+++ b/src/twisted/web/server.py
@@ -335,10 +335,12 @@ def render(self, resrc):
"allowed": ", ".join([nativeString(x) for x in allowedMethods]),
}
)
- epage = resource.ErrorPage(http.NOT_ALLOWED, "Method Not Allowed", s)
+ epage = resource._UnsafeErrorPage(
+ http.NOT_ALLOWED, "Method Not Allowed", s
+ )
body = epage.render(self)
else:
- epage = resource.ErrorPage(
+ epage = resource._UnsafeErrorPage(
http.NOT_IMPLEMENTED,
"Huh?",
"I don't know how to treat a %s request."
@@ -350,10 +352,11 @@ def render(self, resrc):
if body is NOT_DONE_YET:
return
if not isinstance(body, bytes):
- body = resource.ErrorPage(
+ body = resource._UnsafeErrorPage(
http.INTERNAL_SERVER_ERROR,
"Request did not return bytes",
"Request: "
+ # GHSA-vg46-2rrj-3647 note: _PRE does HTML-escape the input.
+ util._PRE(reflect.safe_repr(self))
+ "<br />"
+ "Resource: "
@@ -607,7 +610,7 @@ class GzipEncoderFactory:
@since: 12.3
"""
- _gzipCheckRegex = re.compile(br"(:?^|[\s,])gzip(:?$|[\s,])")
+ _gzipCheckRegex = re.compile(rb"(:?^|[\s,])gzip(:?$|[\s,])")
compressLevel = 9
def encoderForRequest(self, request):
diff --git a/src/twisted/web/static.py b/src/twisted/web/static.py
index 2689d3cfdab..09a2947f911 100644
--- a/src/twisted/web/static.py
+++ b/src/twisted/web/static.py
@@ -31,7 +31,7 @@
from twisted.web import http, resource, server
from twisted.web.util import redirectTo
-dangerousPathError = resource.NoResource("Invalid request URL.")
+dangerousPathError = resource._UnsafeNoResource("Invalid request URL.")
def isDangerous(path):
@@ -255,8 +255,8 @@ def ignoreExt(self, ext):
"""
self.ignoredExts.append(ext)
- childNotFound = resource.NoResource("File not found.")
- forbidden = resource.ForbiddenResource()
+ childNotFound = resource._UnsafeNoResource("File not found.")
+ forbidden = resource._UnsafeForbiddenResource()
def directoryListing(self):
"""
From 404cbc2455c6d25d4570c50e50958c342b3591f9 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 18:32:12 -0700
Subject: [PATCH 04/12] Update the docs
---
docs/web/howto/web-in-60/error-handling.rst | 26 ++++++---------------
1 file changed, 7 insertions(+), 19 deletions(-)
diff --git a/docs/web/howto/web-in-60/error-handling.rst b/docs/web/howto/web-in-60/error-handling.rst
index 7717119f898..7cf1a789fec 100644
--- 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 with :py:class:`Site <twisted.web.serve
-Next, we'll add one more import. :py:class:`NoResource <twisted.web.resource.NoResource>` is one of the pre-defined error
+Next, we'll add one more import. :py:class:`notFound <twisted.web.pages.notFound>` is one of the pre-defined error
resources provided by Twisted Web. It generates the necessary 404 response code
-and renders a simple html page telling the client there is no such resource.
-
-
-
-
+and renders a simple HTML page telling the client there is no such resource.
.. code-block:: python
-
- from twisted.web.resource import NoResource
-
-
-
+ from twisted.web.pages import notFound
Next, we'll define a custom resource which does some dynamic URL
dispatch. This example is going to be just like
@@ -54,10 +46,6 @@ the :doc:`previous one <dynamic-dispatch>` , where the path segment is
interpreted as a year; the difference is that this time we'll handle requests
which don't conform to that pattern by returning the not found response:
-
-
-
-
.. code-block:: python
@@ -66,7 +54,7 @@ which don't conform to that pattern by returning the not found response:
try:
year = int(name)
except ValueError:
- return NoResource()
+ return notFound()
else:
return YearPage(year)
@@ -88,7 +76,7 @@ complete code for this example:
from twisted.web.server import Site
from twisted.web.resource import Resource
from twisted.internet import reactor, endpoints
- from twisted.web.resource import NoResource
+ from twisted.web.pages import notFound
from calendar import calendar
@@ -100,14 +88,14 @@ complete code for this example:
def render_GET(self, request):
cal = calendar(self.year)
return (b"<!DOCTYPE html><html><head><meta charset='utf-8'>"
- b"<title></title></head><body><pre>" + cal.encode('utf-8') + "</pre>")
+ b"<title></title></head><body><pre>" + cal.encode('utf-8') + b"</pre>")
class Calendar(Resource):
def getChild(self, name, request):
try:
year = int(name)
except ValueError:
- return NoResource()
+ return notFound()
else:
return YearPage(year)
From 6cf64b7782e92efeafc5e17e1b59e3bfc1e70c47 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 18:45:06 -0700
Subject: [PATCH 05/12] Address DummyRequest MyPy issue
Filed https://github.com/twisted/twisted/issues/11719 for this.
---
src/twisted/web/test/test_pages.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/twisted/web/test/test_pages.py b/src/twisted/web/test/test_pages.py
index d83e0eba3e2..1f66c16afc9 100644
--- a/src/twisted/web/test/test_pages.py
+++ b/src/twisted/web/test/test_pages.py
@@ -5,8 +5,11 @@
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.test.requesthelper import DummyRequest
@@ -20,7 +23,10 @@ def _render(resource: ErrorPage) -> DummyRequest:
@returns: The request that the resource handled,
"""
request = DummyRequest([b""])
- resource.render(request)
+ # 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
From 8a6437541e0a282ad13fa8ab3cb23e4cddae9bda Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 19:25:51 -0700
Subject: [PATCH 06/12] Address IRenderable MyPy issue
---
src/twisted/web/pages.py | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/src/twisted/web/pages.py b/src/twisted/web/pages.py
index 8f37b3e45a8..524148fba26 100644
--- a/src/twisted/web/pages.py
+++ b/src/twisted/web/pages.py
@@ -12,9 +12,10 @@
"forbidden",
)
+from typing import cast
from twisted.web import http
-from twisted.web.iweb import IRequest
+from twisted.web.iweb import IRenderable, IRequest
from twisted.web.resource import Resource
from twisted.web.template import renderElement, tags
@@ -50,18 +51,24 @@ def __init__(self, code: int, brief: str, detail: str) -> None:
self._brief: str = brief
self._detail: str = detail
- def render(self, request: IRequest) -> None:
+ 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")
- renderElement(
+ return renderElement(
request,
- tags.html(
- tags.head(tags.title(f"{self._code} - {self._brief}")),
- tags.body(tags.h1(self._brief), tags.p(self._detail)),
+ # 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)),
+ ),
),
)
From a85a4904439a2f4783f0f2a85fc73c3c7837ac64 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 19:45:24 -0700
Subject: [PATCH 07/12] Failing test
---
src/twisted/web/test/test_vhost.py | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/src/twisted/web/test/test_vhost.py b/src/twisted/web/test/test_vhost.py
index f26d5e5d5e9..bb66b55537a 100644
--- a/src/twisted/web/test/test_vhost.py
+++ b/src/twisted/web/test/test_vhost.py
@@ -66,7 +66,7 @@ def test_renderWithoutHost(self):
"""
virtualHostResource = NameVirtualHost()
virtualHostResource.default = Data(b"correct result", "")
- request = DummyRequest([""])
+ request = DummyRequest([b""])
self.assertEqual(virtualHostResource.render(request), b"correct result")
def test_renderWithoutHostNoDefault(self):
@@ -76,7 +76,7 @@ def test_renderWithoutHostNoDefault(self):
header in the request.
"""
virtualHostResource = NameVirtualHost()
- request = DummyRequest([""])
+ request = DummyRequest([b""])
d = _render(virtualHostResource, request)
def cbRendered(ignored):
@@ -140,7 +140,7 @@ def test_renderWithUnknownHostNoDefault(self):
matching the value of the I{Host} header in the request.
"""
virtualHostResource = NameVirtualHost()
- request = DummyRequest([""])
+ request = DummyRequest([b""])
request.requestHeaders.addRawHeader(b"host", b"example.com")
d = _render(virtualHostResource, request)
@@ -150,6 +150,19 @@ def cbRendered(ignored):
d.addCallback(cbRendered)
return d
+ async def test_renderWithHTMLHost(self):
+ """
+ L{NameVirtualHost.render} doesn't echo unescaped HTML when present in
+ the I{Host} header.
+ """
+ virtualHostResource = NameVirtualHost()
+ request = DummyRequest([b""])
+ request.requestHeaders.addRawHeader(b"host", b"<b>example</b>.com")
+
+ await _render(virtualHostResource, request)
+
+ self.assertNotIn(b"<b>", b"".join(request.written))
+
def test_getChild(self):
"""
L{NameVirtualHost.getChild} returns correct I{Resource} based off
From d766a02b053d786e224b4d5449149bdd1bbedf84 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Sat, 22 Oct 2022 20:01:50 -0700
Subject: [PATCH 08/12] Fix NameVirtualHost HTML injection vulnerability
---
src/twisted/web/newsfragments/11716.bugfix | 1 +
src/twisted/web/vhost.py | 11 ++++++-----
2 files changed, 7 insertions(+), 5 deletions(-)
create mode 100644 src/twisted/web/newsfragments/11716.bugfix
diff --git a/src/twisted/web/newsfragments/11716.bugfix b/src/twisted/web/newsfragments/11716.bugfix
new file mode 100644
index 00000000000..66189e685c5
--- /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 (GHSA-vg46-2rrj-3647).
diff --git a/src/twisted/web/vhost.py b/src/twisted/web/vhost.py
index 2c305f94374..9576252b0f2 100644
--- a/src/twisted/web/vhost.py
+++ b/src/twisted/web/vhost.py
@@ -9,7 +9,7 @@
# Twisted Imports
from twisted.python import roots
-from twisted.web import resource
+from twisted.web import pages, resource
class VirtualHostCollection(roots.Homogenous):
@@ -77,12 +77,13 @@ def removeHost(self, name):
def _getResourceForRequest(self, request):
"""(Internal) Get the appropriate resource for the given host."""
hostHeader = request.getHeader(b"host")
- if hostHeader == None:
- return self.default or resource.NoResource()
+ if hostHeader is None:
+ return self.default or pages.notFound()
else:
host = hostHeader.lower().split(b":", 1)[0]
- return self.hosts.get(host, self.default) or resource.NoResource(
- "host %s not in vhost map" % repr(host)
+ return self.hosts.get(host, self.default) or pages.notFound(
+ "Not Found",
+ f"host {host.decode('ascii', 'replace')!r} not in vhost map",
)
def render(self, request):
From fee019520ebe31b79c904237e4ac3a7c86d65461 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 24 Oct 2022 21:14:07 -0700
Subject: [PATCH 09/12] Fix references to twisted.pages
---
src/twisted/web/resource.py | 12 ++++++------
src/twisted/web/test/test_resource.py | 6 +++---
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py
index 09fc74a89fd..f196c3ac6b9 100644
--- a/src/twisted/web/resource.py
+++ b/src/twisted/web/resource.py
@@ -299,7 +299,7 @@ class _UnsafeErrorPage(Resource):
descriptive text. This is useful for rendering simple error pages.
Deprecated in Twisted NEXT because it permits HTML injection; use
- L{twisted.pages.ErrorPage} instead.
+ 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
@@ -359,7 +359,7 @@ class _UnsafeNoResource(_UnsafeErrorPage):
returns the HTTP response code I{NOT FOUND}.
Deprecated in Twisted NEXT because it permits HTML injection; use
- L{twisted.pages.notFound} instead.
+ L{twisted.web.pages.notFound} instead.
"""
def __init__(self, message="Sorry. No luck finding that resource."):
@@ -373,7 +373,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage):
returns the I{FORBIDDEN} HTTP response code.
Deprecated in Twisted NEXT because it permits HTML injection; use
- L{twisted.pages.forbidden} instead.
+ L{twisted.web.pages.forbidden} instead.
"""
def __init__(self, message="Sorry, resource is forbidden."):
@@ -387,21 +387,21 @@ def __init__(self, message="Sorry, resource is forbidden."):
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.pages.ErrorPage instead, which properly escapes HTML.",
+ "Use twisted.web.pages.ErrorPage instead, which properly escapes HTML.",
__name__,
"ErrorPage",
)
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.pages.notFound instead, which properly escapes HTML.",
+ "Use twisted.web.pages.notFound instead, which properly escapes HTML.",
__name__,
"NoResource",
)
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.pages.forbidden instead, which properly escapes HTML.",
+ "Use twisted.web.pages.forbidden instead, which properly escapes HTML.",
__name__,
"ForbiddenResource",
)
diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py
index c039704a79d..cb37942dbb8 100644
--- a/src/twisted/web/test/test_resource.py
+++ b/src/twisted/web/test/test_resource.py
@@ -42,7 +42,7 @@ def test_deprecatedErrorPage(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.pages.ErrorPage", warning["message"])
+ self.assertIn("twisted.web.pages.ErrorPage", warning["message"])
def test_deprecatedNoResource(self):
"""
@@ -56,7 +56,7 @@ def test_deprecatedNoResource(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.pages.notFound", warning["message"])
+ self.assertIn("twisted.web.pages.notFound", warning["message"])
def test_deprecatedForbiddenResource(self):
"""
@@ -70,7 +70,7 @@ def test_deprecatedForbiddenResource(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.pages.forbidden", warning["message"])
+ self.assertIn("twisted.web.pages.forbidden", warning["message"])
def test_getChild(self):
"""
From 09ce75e3a7ee0675812ccd7e7164fb4a39970e38 Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 24 Oct 2022 21:25:21 -0700
Subject: [PATCH 10/12] Call the superclass constructor via private alias
---
src/twisted/web/resource.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py
index f196c3ac6b9..de3b557048a 100644
--- a/src/twisted/web/resource.py
+++ b/src/twisted/web/resource.py
@@ -363,7 +363,7 @@ class _UnsafeNoResource(_UnsafeErrorPage):
"""
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 _UnsafeForbiddenResource(_UnsafeErrorPage):
@@ -377,7 +377,7 @@ class _UnsafeForbiddenResource(_UnsafeErrorPage):
"""
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.
From c0da7805fbb30611df8ac1bce3dffa2c6659373c Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 24 Oct 2022 21:42:30 -0700
Subject: [PATCH 11/12] =?UTF-8?q?twisted.web.pages.{ErrorPage=20=E2=86=92?=
=?UTF-8?q?=20errorPage}?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/twisted/web/newsfragments/11716.feature | 2 +-
src/twisted/web/pages.py | 69 +++++++++++++--------
src/twisted/web/resource.py | 4 +-
src/twisted/web/test/test_pages.py | 15 ++---
src/twisted/web/test/test_resource.py | 2 +-
5 files changed, 56 insertions(+), 36 deletions(-)
diff --git a/src/twisted/web/newsfragments/11716.feature b/src/twisted/web/newsfragments/11716.feature
index e8ba00b7ef2..bdcd36d17bd 100644
--- a/src/twisted/web/newsfragments/11716.feature
+++ b/src/twisted/web/newsfragments/11716.feature
@@ -1 +1 @@
-The twisted.web.pages.ErrorPage, notFound, and forbidden IResource implementations provide HTML error pages safely rendered using twisted.web.template.
+The twisted.web.pages.errorPage, notFound, and forbidden each return an IResource that displays an HTML error pages safely rendered using twisted.web.template.
diff --git a/src/twisted/web/pages.py b/src/twisted/web/pages.py
index 524148fba26..002b8a95d90 100644
--- a/src/twisted/web/pages.py
+++ b/src/twisted/web/pages.py
@@ -7,7 +7,7 @@
"""
__all__ = (
- "ErrorPage",
+ "errorPage",
"notFound",
"forbidden",
)
@@ -16,15 +16,17 @@
from twisted.web import http
from twisted.web.iweb import IRenderable, IRequest
-from twisted.web.resource import Resource
+from twisted.web.resource import IResource, Resource
from twisted.web.template import renderElement, tags
-class ErrorPage(Resource):
+class _ErrorPage(Resource):
"""
- L{ErrorPage} is a resource that responds to all requests with a particular
- (parameterized) HTTP status code and a body consisting of HTML containing
- some descriptive text. This is useful for rendering simple error pages.
+ 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.
@@ -36,16 +38,6 @@ class ErrorPage(Resource):
"""
def __init__(self, code: int, brief: str, detail: str) -> None:
- """
- @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.
- """
super().__init__()
self._code: int = code
self._brief: str = brief
@@ -74,7 +66,7 @@ def render(self, request: IRequest) -> object:
def getChild(self, path: bytes, request: IRequest) -> Resource:
"""
- Handle all requests for which L{ErrorPage} lacks a child by returning
+ Handle all requests for which L{_ErrorPage} lacks a child by returning
this error page.
@param path: A path segment.
@@ -84,32 +76,59 @@ def getChild(self, path: bytes, request: IRequest) -> Resource:
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.",
-) -> ErrorPage:
+) -> IResource:
"""
- Generate an L{ErrorPage} with a 404 Not Found status code.
+ 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{ErrorPage}
+ @returns: An L{IResource}
"""
- return ErrorPage(http.NOT_FOUND, brief, message)
+ return _ErrorPage(http.NOT_FOUND, brief, message)
def forbidden(
brief: str = "Forbidden Resource", message: str = "Sorry, resource is forbidden."
-) -> ErrorPage:
+) -> IResource:
"""
- Generate an L{ErrorPage} with a 403 Forbidden status code.
+ 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{ErrorPage}
+ @returns: An L{IResource}
"""
- return ErrorPage(http.FORBIDDEN, brief, message)
+ return _ErrorPage(http.FORBIDDEN, brief, message)
diff --git a/src/twisted/web/resource.py b/src/twisted/web/resource.py
index de3b557048a..670940f2086 100644
--- a/src/twisted/web/resource.py
+++ b/src/twisted/web/resource.py
@@ -299,7 +299,7 @@ class _UnsafeErrorPage(Resource):
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.
+ 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
@@ -387,7 +387,7 @@ def __init__(self, message="Sorry, resource is forbidden."):
deprecatedModuleAttribute(
Version("Twisted", "NEXT", 0, 0),
- "Use twisted.web.pages.ErrorPage instead, which properly escapes HTML.",
+ "Use twisted.web.pages.errorPage instead, which properly escapes HTML.",
__name__,
"ErrorPage",
)
diff --git a/src/twisted/web/test/test_pages.py b/src/twisted/web/test/test_pages.py
index 1f66c16afc9..acd9b978fe0 100644
--- a/src/twisted/web/test/test_pages.py
+++ b/src/twisted/web/test/test_pages.py
@@ -10,11 +10,12 @@
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.pages import errorPage, forbidden, notFound
+from twisted.web.resource import IResource
from twisted.web.test.requesthelper import DummyRequest
-def _render(resource: ErrorPage) -> DummyRequest:
+def _render(resource: IResource) -> DummyRequest:
"""
Render a response using the given resource.
@@ -32,7 +33,7 @@ def _render(resource: ErrorPage) -> DummyRequest:
class ErrorPageTests(SynchronousTestCase):
"""
- Test L{twisted.web.pages.ErrorPage} and its convencience helpers
+ Test L{twisted.web.pages._ErrorPage} and its public aliases L{errorPage},
L{notFound} and L{forbidden}.
"""
@@ -56,7 +57,7 @@ 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!')")),
+ _render(errorPage(400, "A & B", "<script>alert('oops!')")),
400,
(
b"<!DOCTYPE html>\n"
@@ -68,10 +69,10 @@ def test_escapesHTML(self):
def test_getChild(self):
"""
- The C{getChild} method of L{ErrorPage} returns the L{ErrorPage} it is
- called on.
+ The C{getChild} method of the resource returned by L{errorPage} returns
+ the L{_ErrorPage} it is called on.
"""
- page = ErrorPage(404, "foo", "bar")
+ page = errorPage(404, "foo", "bar")
self.assertIs(
page.getChild(b"name", DummyRequest([b""])),
page,
diff --git a/src/twisted/web/test/test_resource.py b/src/twisted/web/test/test_resource.py
index cb37942dbb8..72e9137c1c8 100644
--- a/src/twisted/web/test/test_resource.py
+++ b/src/twisted/web/test/test_resource.py
@@ -42,7 +42,7 @@ def test_deprecatedErrorPage(self):
[warning] = self.flushWarnings()
self.assertEqual(warning["category"], DeprecationWarning)
- self.assertIn("twisted.web.pages.ErrorPage", warning["message"])
+ self.assertIn("twisted.web.pages.errorPage", warning["message"])
def test_deprecatedNoResource(self):
"""
From 78662333e70eb1b440e7d0025f4fc376709c19ac Mon Sep 17 00:00:00 2001
From: Tom Most <twm@freecog.net>
Date: Mon, 24 Oct 2022 22:30:42 -0700
Subject: [PATCH 12/12] Add CVE to newsfragment
---
src/twisted/web/newsfragments/11716.bugfix | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/twisted/web/newsfragments/11716.bugfix b/src/twisted/web/newsfragments/11716.bugfix
index 66189e685c5..5264c8fc202 100644
--- a/src/twisted/web/newsfragments/11716.bugfix
+++ b/src/twisted/web/newsfragments/11716.bugfix
@@ -1 +1 @@
-twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (GHSA-vg46-2rrj-3647).
+twisted.web.vhost.NameVirtualHost no longer echoes HTML received in the Host header without escaping it (CVE-2022-39348, GHSA-vg46-2rrj-3647).