1450 lines
53 KiB
Diff
1450 lines
53 KiB
Diff
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).
|