Repository: pallets/werkzeug (python)

Issue:
werkzeug double-encodes redirects when processing merge_slashes
```python
from werkzeug.routing import Rule, Map

routes = Map([
    Rule("/merge/<some>/path"),
])

def get_redirect_location(path):
    try:
        routes.bind("test.host").match(path)
    except Exception as e:
        return e.get_response({}).headers['location']
    else:
        raise ValueError("no redirect")

def test_merge_slashes():
    assert get_redirect_location("/merge/%//path") == 'http://test.host/merge/%25/path'
```

It looks like `Rule.match` uses `Rule.build` to generate the new path [here](https://github.com/pallets/werkzeug/blob/2.0.1/src/werkzeug/routing.py#L943), which is already urlencoded. But then `MapAdapter.match` encodes the resulting path again [here](https://github.com/pallets/werkzeug/blob/2.0.1/src/werkzeug/routing.py#L1963-L1969).

Environment:

- Python version: 3.9.5
- Werkzeug version: 2.0.1

Pull request:
Fix double encoding of URLs on redirects when merge_slash=True
Rule.match uses Rule.build to generate a new urlencoded path [here](https://github.com/pallets/werkzeug/blob/2.0.1/src/werkzeug/routing.py#L943). But then MapAdapter.match encodes the resulting path again [here](https://github.com/pallets/werkzeug/blob/2.0.1/src/werkzeug/routing.py#L1963-L1969).

This fix uses `url_unquote` to decode the URL before passing to MapAdapter.match to prevent double encoding.

- fixes #2157 

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [ ] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [ ] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index bd4e4694..bf157b56 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -8,8 +8,10 @@ Unreleased
 -   Handle multiple tokens in ``Connection`` header when routing
     WebSocket requests. :issue:`2131`
 -   Set the debugger pin cookie secure flag when on https. :pr:`2150`
--   Fix type annotation for ``MultiDict.update`` to accept iterable values
-    :pr:`2142`
+-   Fix type annotation for ``MultiDict.update`` to accept iterable
+    values :pr:`2142`
+-   Prevent double encoding of redirect URL when ``merge_slash=True``
+    for ``Rule.match``. :issue:`2157`
 
 
 Version 2.0.1
diff --git a/src/werkzeug/routing.py b/src/werkzeug/routing.py
index 345871ee..5c456c34 100644
--- a/src/werkzeug/routing.py
+++ b/src/werkzeug/routing.py
@@ -134,6 +134,7 @@ from .urls import _fast_url_quote
 from .urls import url_encode
 from .urls import url_join
 from .urls import url_quote
+from .urls import url_unquote
 from .utils import cached_property
 from .utils import redirect
 from .wsgi import get_host
@@ -944,7 +945,10 @@ class Rule(RuleFactory):
                     if path.endswith("/") and not new_path.endswith("/"):
                         new_path += "/"
                     if new_path.count("/") < path.count("/"):
-                        path = new_path
+                        # The URL will be encoded when MapAdapter.match
+                        # handles the RequestPath raised below. Decode
+                        # the URL here to avoid a double encoding.
+                        path = url_unquote(new_path)
                         require_redirect = True
 
                 if require_redirect:


Test patch:
diff --git a/tests/test_routing.py b/tests/test_routing.py
index 1d4cef90..fb343ee8 100644
--- a/tests/test_routing.py
+++ b/tests/test_routing.py
@@ -122,6 +122,26 @@ def test_merge_slashes_match():
     assert adapter.match("/no//merge")[0] == "no_merge"
 
 
+@pytest.mark.parametrize(
+    ("path", "expected"),
+    [("/merge/%//path", "/merge/%25/path"), ("/merge//st/path", "/merge/st/path")],
+)
+def test_merge_slash_encoding(path, expected):
+    """This test is to make sure URLs are not double-encoded
+    when a redirect is thrown with `merge_slash = True`"""
+    url_map = r.Map(
+        [
+            r.Rule("/merge/<some>/path"),
+        ]
+    )
+    adapter = url_map.bind("localhost", "/")
+
+    with pytest.raises(r.RequestRedirect) as excinfo:
+        adapter.match(path)
+
+    assert excinfo.value.new_url.endswith(expected)
+
+
 def test_merge_slashes_build():
     url_map = r.Map(
         [