From e075bba892f6f1bf72eef2de945286df974afb88 Mon Sep 17 00:00:00 2001 From: pgjones Date: Sun, 3 Mar 2024 16:38:25 +0000 Subject: [PATCH] Fix issue with repeated-slash requests redirecting Previously if a request had repeated slashes it could match a single slash route and hence return a redirect response even if merge_slashes was False. Additionally setting the merge_slashes attribute of the map after initialisation had no affect, compounding this problem. --- CHANGES.rst | 2 ++ src/werkzeug/routing/map.py | 9 ++++++++- src/werkzeug/routing/matcher.py | 2 +- tests/test_routing.py | 4 ++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 3e9993488..c7f5aeba5 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,8 @@ Version 3.0.2 Unreleased +- Ensure setting merge_slashes to False results in NotFound for + repeated-slash requests against single slash routes. :issue:`2834` - Fix handling of TypeError in TypeConversionDict.get() to match ValueErrors. :issue:`2843` - Fix response_wrapper type check in test client. :issue:`2831` diff --git a/src/werkzeug/routing/map.py b/src/werkzeug/routing/map.py index 76bbe2f3b..87b83a54c 100644 --- a/src/werkzeug/routing/map.py +++ b/src/werkzeug/routing/map.py @@ -109,7 +109,6 @@ def __init__( self.default_subdomain = default_subdomain self.strict_slashes = strict_slashes - self.merge_slashes = merge_slashes self.redirect_defaults = redirect_defaults self.host_matching = host_matching @@ -123,6 +122,14 @@ def __init__( for rulefactory in rules or (): self.add(rulefactory) + @property + def merge_slashes(self) -> bool: + return self._matcher.merge_slashes + + @merge_slashes.setter + def merge_slashes(self, value: bool) -> None: + self._matcher.merge_slashes = value + def is_endpoint_expecting(self, endpoint: str, *arguments: str) -> bool: """Iterate over all rules and check if the endpoint expects the arguments provided. This is for example useful if you have diff --git a/src/werkzeug/routing/matcher.py b/src/werkzeug/routing/matcher.py index 0d1210a67..1fd00efca 100644 --- a/src/werkzeug/routing/matcher.py +++ b/src/werkzeug/routing/matcher.py @@ -177,7 +177,7 @@ def _match( rv = _match(self._root, [domain, *path.split("/")], []) except SlashRequired: raise RequestPath(f"{path}/") from None - if rv is None: + if rv is None or rv[0].merge_slashes is False: raise NoMatch(have_match_for, websocket_mismatch) else: raise RequestPath(f"{path}") diff --git a/tests/test_routing.py b/tests/test_routing.py index 416fb4fc5..5291348c0 100644 --- a/tests/test_routing.py +++ b/tests/test_routing.py @@ -95,6 +95,7 @@ def test_merge_slashes_match(): r.Rule("/yes/tail/", endpoint="yes_tail"), r.Rule("/with/", endpoint="with_path"), r.Rule("/no//merge", endpoint="no_merge", merge_slashes=False), + r.Rule("/no/merging", endpoint="no_merging", merge_slashes=False), ] ) adapter = url_map.bind("localhost", "/") @@ -124,6 +125,9 @@ def test_merge_slashes_match(): assert adapter.match("/no//merge")[0] == "no_merge" + assert adapter.match("/no/merging")[0] == "no_merging" + pytest.raises(NotFound, lambda: adapter.match("/no//merging")) + @pytest.mark.parametrize( ("path", "expected"),