Skip to content

Commit 4c410bb

Browse files
[AUTO-CHERRYPICK] python-werkzeug: Patch CVE-2024-34069 - branch main (#9118)
Co-authored-by: Jonathan Behrens <jbehrens@microsoft.com>
1 parent 14d8692 commit 4c410bb

2 files changed

Lines changed: 208 additions & 1 deletion

File tree

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
From 404082ffe6f1ef9541d01434aebb789363567df9 Mon Sep 17 00:00:00 2001
2+
From: David Lord <davidism@gmail.com>
3+
Date: Thu, 2 May 2024 11:55:52 -0700
4+
Subject: [PATCH 1/2] restrict debugger trusted hosts
5+
6+
Add a list of `trusted_hosts` to the `DebuggedApplication` middleware. It defaults to only allowing `localhost`, `.localhost` subdomains, and `127.0.0.1`. `run_simple(use_debugger=True)` adds its `hostname` argument to the trusted list as well. The middleware can be used directly to further modify the trusted list in less common development scenarios.
7+
8+
The debugger UI uses the full `document.location` instead of only `document.location.pathname`.
9+
10+
Either of these fixes on their own mitigates the reported vulnerability.
11+
---
12+
src/werkzeug/debug/__init__.py | 10 ++++++++++
13+
src/werkzeug/debug/shared/debugger.js | 4 ++--
14+
src/werkzeug/serving.py | 3 +++
15+
3 files changed, 15 insertions(+), 2 deletions(-)
16+
17+
diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py
18+
index 3b04b534..c5fffdec 100644
19+
--- a/src/werkzeug/debug/__init__.py
20+
+++ b/src/werkzeug/debug/__init__.py
21+
@@ -297,6 +297,14 @@ class DebuggedApplication:
22+
else:
23+
self.pin = None
24+
25+
+ self.trusted_hosts: list[str] = [".localhost", "127.0.0.1"]
26+
+ """List of domains to allow requests to the debugger from. A leading dot
27+
+ allows all subdomains. This only allows ``".localhost"`` domains by
28+
+ default.
29+
+
30+
+ .. versionadded:: 3.0.3
31+
+ """
32+
+
33+
@property
34+
def pin(self) -> str | None:
35+
if not hasattr(self, "_pin"):
36+
@@ -505,6 +513,8 @@ class DebuggedApplication:
37+
# form data! Otherwise the application won't have access to that data
38+
# any more!
39+
request = Request(environ)
40+
+ request.trusted_hosts = self.trusted_hosts
41+
+ assert request.host # will raise 400 error if not trusted
42+
response = self.debug_application
43+
if request.args.get("__debugger__") == "yes":
44+
cmd = request.args.get("cmd")
45+
diff --git a/src/werkzeug/debug/shared/debugger.js b/src/werkzeug/debug/shared/debugger.js
46+
index f463e9c7..18c65834 100644
47+
--- a/src/werkzeug/debug/shared/debugger.js
48+
+++ b/src/werkzeug/debug/shared/debugger.js
49+
@@ -48,7 +48,7 @@ function initPinBox() {
50+
btn.disabled = true;
51+
52+
fetch(
53+
- `${document.location.pathname}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
54+
+ `${document.location}?__debugger__=yes&cmd=pinauth&pin=${pin}&s=${encodedSecret}`
55+
)
56+
.then((res) => res.json())
57+
.then(({auth, exhausted}) => {
58+
@@ -79,7 +79,7 @@ function promptForPin() {
59+
if (!EVALEX_TRUSTED) {
60+
const encodedSecret = encodeURIComponent(SECRET);
61+
fetch(
62+
- `${document.location.pathname}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
63+
+ `${document.location}?__debugger__=yes&cmd=printpin&s=${encodedSecret}`
64+
);
65+
const pinPrompt = document.getElementsByClassName("pin-prompt")[0];
66+
fadeIn(pinPrompt);
67+
diff --git a/src/werkzeug/serving.py b/src/werkzeug/serving.py
68+
index c031dc45..f940ca38 100644
69+
--- a/src/werkzeug/serving.py
70+
+++ b/src/werkzeug/serving.py
71+
@@ -1066,6 +1066,9 @@ def run_simple(
72+
from .debug import DebuggedApplication
73+
74+
application = DebuggedApplication(application, evalex=use_evalex)
75+
+ # Allow the specified hostname to use the debugger, in addition to
76+
+ # localhost domains.
77+
+ application.trusted_hosts.append(hostname)
78+
79+
if not is_running_from_reloader():
80+
fd = None
81+
--
82+
2.34.1
83+
84+
85+
From 813580c5d9c7b18d8df5cfe042034eba60f794f4 Mon Sep 17 00:00:00 2001
86+
From: David Lord <davidism@gmail.com>
87+
Date: Fri, 3 May 2024 14:49:43 -0700
88+
Subject: [PATCH 2/2] only require trusted host for evalex
89+
90+
---
91+
src/werkzeug/debug/__init__.py | 25 ++++++++++++++++++++-----
92+
src/werkzeug/sansio/utils.py | 2 +-
93+
2 files changed, 21 insertions(+), 6 deletions(-)
94+
95+
diff --git a/src/werkzeug/debug/__init__.py b/src/werkzeug/debug/__init__.py
96+
index c5fffdec..c90d94d7 100644
97+
--- a/src/werkzeug/debug/__init__.py
98+
+++ b/src/werkzeug/debug/__init__.py
99+
@@ -19,7 +19,9 @@ from zlib import adler32
100+
101+
from .._internal import _log
102+
from ..exceptions import NotFound
103+
+from ..exceptions import SecurityError
104+
from ..http import parse_cookie
105+
+from ..sansio.utils import host_is_trusted
106+
from ..security import gen_salt
107+
from ..utils import send_file
108+
from ..wrappers.request import Request
109+
@@ -351,7 +353,7 @@ class DebuggedApplication:
110+
111+
is_trusted = bool(self.check_pin_trust(environ))
112+
html = tb.render_debugger_html(
113+
- evalex=self.evalex,
114+
+ evalex=self.evalex and self.check_host_trust(environ),
115+
secret=self.secret,
116+
evalex_trusted=is_trusted,
117+
)
118+
@@ -379,6 +381,9 @@ class DebuggedApplication:
119+
frame: DebugFrameSummary | _ConsoleFrame,
120+
) -> Response:
121+
"""Execute a command in a console."""
122+
+ if not self.check_host_trust(request.environ):
123+
+ return SecurityError() # type: ignore[return-value]
124+
+
125+
contexts = self.frame_contexts.get(id(frame), [])
126+
127+
with ExitStack() as exit_stack:
128+
@@ -389,6 +394,9 @@ class DebuggedApplication:
129+
130+
def display_console(self, request: Request) -> Response:
131+
"""Display a standalone shell."""
132+
+ if not self.check_host_trust(request.environ):
133+
+ return SecurityError() # type: ignore[return-value]
134+
+
135+
if 0 not in self.frames:
136+
if self.console_init_func is None:
137+
ns = {}
138+
@@ -441,12 +449,18 @@ class DebuggedApplication:
139+
return None
140+
return (time.time() - PIN_TIME) < ts
141+
142+
+ def check_host_trust(self, environ: WSGIEnvironment) -> bool:
143+
+ return host_is_trusted(environ.get("HTTP_HOST"), self.trusted_hosts)
144+
+
145+
def _fail_pin_auth(self) -> None:
146+
time.sleep(5.0 if self._failed_pin_auth > 5 else 0.5)
147+
self._failed_pin_auth += 1
148+
149+
def pin_auth(self, request: Request) -> Response:
150+
"""Authenticates with the pin."""
151+
+ if not self.check_host_trust(request.environ):
152+
+ return SecurityError() # type: ignore[return-value]
153+
+
154+
exhausted = False
155+
auth = False
156+
trust = self.check_pin_trust(request.environ)
157+
@@ -496,8 +510,11 @@ class DebuggedApplication:
158+
rv.delete_cookie(self.pin_cookie_name)
159+
return rv
160+
161+
- def log_pin_request(self) -> Response:
162+
+ def log_pin_request(self, request: Request) -> Response:
163+
"""Log the pin if needed."""
164+
+ if not self.check_host_trust(request.environ):
165+
+ return SecurityError() # type: ignore[return-value]
166+
+
167+
if self.pin_logging and self.pin is not None:
168+
_log(
169+
"info", " * To enable the debugger you need to enter the security pin:"
170+
@@ -513,8 +530,6 @@ class DebuggedApplication:
171+
# form data! Otherwise the application won't have access to that data
172+
# any more!
173+
request = Request(environ)
174+
- request.trusted_hosts = self.trusted_hosts
175+
- assert request.host # will raise 400 error if not trusted
176+
response = self.debug_application
177+
if request.args.get("__debugger__") == "yes":
178+
cmd = request.args.get("cmd")
179+
@@ -526,7 +541,7 @@ class DebuggedApplication:
180+
elif cmd == "pinauth" and secret == self.secret:
181+
response = self.pin_auth(request) # type: ignore
182+
elif cmd == "printpin" and secret == self.secret:
183+
- response = self.log_pin_request() # type: ignore
184+
+ response = self.log_pin_request(request) # type: ignore
185+
elif (
186+
self.evalex
187+
and cmd is not None
188+
diff --git a/src/werkzeug/sansio/utils.py b/src/werkzeug/sansio/utils.py
189+
index 48ec1bfa..14fa0ac8 100644
190+
--- a/src/werkzeug/sansio/utils.py
191+
+++ b/src/werkzeug/sansio/utils.py
192+
@@ -8,7 +8,7 @@ from ..exceptions import SecurityError
193+
from ..urls import uri_to_iri
194+
195+
196+
-def host_is_trusted(hostname: str, trusted_list: t.Iterable[str]) -> bool:
197+
+def host_is_trusted(hostname: str | None, trusted_list: t.Iterable[str]) -> bool:
198+
"""Check if a host matches a list of trusted names.
199+
200+
:param hostname: The name to check.
201+
--
202+
2.34.1
203+

SPECS/python-werkzeug/python-werkzeug.spec

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Summary: The Swiss Army knife of Python web development
22
Name: python-werkzeug
33
Version: 2.3.7
4-
Release: 1%{?dist}
4+
Release: 2%{?dist}
55
License: BSD
66
Vendor: Microsoft Corporation
77
Distribution: Mariner
@@ -18,6 +18,7 @@ Patch0: 0001-enable-tests-in-rpm-env.patch
1818
# and are excluded.
1919
Patch1: 0002-disable-stat-test.patch
2020
Patch2: CVE-2023-46136.patch
21+
Patch3: CVE-2024-34069.patch
2122
BuildArch: noarch
2223

2324
%description
@@ -70,6 +71,9 @@ pip3 install -r requirements/tests.txt
7071
%license LICENSE.rst
7172

7273
%changelog
74+
* Tue May 14 2024 Jonathan Behrens <jbehrens@microsoft.com> - 2.3.7-2
75+
- Patch CVE-2024-34069
76+
7377
* Mon Nov 06 2023 Nick Samson <nisamson@microsoft.com> - 2.3.7-1
7478
- Upgraded to version 2.3.7
7579
- Migrated to pyproject build

0 commit comments

Comments
 (0)