WebOb: Location header normalization during redirect leads to open redirect - again
Description
Impact
When WebOb normalizes the HTTP Location header to include the request hostname, it does so by parsing the URL that the user is to be redirected to with Python's urllib.parse, and joining it to the base URL. urlsplit (called internally by urljoin) however treats a // at the start of a string as a URI without a scheme, and then treats the next part as the hostname. urljoin will then use that hostname from the second part as the hostname replacing the original one from the request.
In a previous advisory https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3 an attempt to fix this was made by forcing the replacement of // with /%2f, however this did not take into account that since Python 3.10 urlsplit internally strips ASCII tab, carriage return, and newline characters from the string, so /\t/attacker.com gets turned into //attacker.com and the attacker is able to bypass the changes introduced in that previous advisory, thereby bringing back the problem that was attempted to be fixed.
>>> parse.urlparse("//attacker.com/some/path")
ParseResult(scheme='', netloc='attacker.com', path='/some/path', params='', query='', fragment='')
WebOb uses urljoin to take the request URI and join the redirect location to it, so assuming the request URI is https://example.org/ and the URL to redirect to is /\t/attacker.com/some/path/:
>>> parse.urljoin("https://example.org/", "/\t/attacker.com/some/path/")
'https://attacker.com/some/path/'
Which redirects from example.org where we want the user to stay to attacker.com.
Patches
This issue has been fixed in WebOb 1.8.10.
Workarounds
Any use of the Response class that includes a location can be rewritten to make sure to always pass a full URI that includes the hostname to redirect the user to, or to validate that the redirect target starts with a scheme (e.g. http:// or https://) before assigning to Response.location.
References
- https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3
- CVE-2024-42353
Thanks
- Caleb Brown of Google
Affected products
2Patches
22b9fbedafb31Fix open redirect issue due to changes made in cPython >=3.10
3 files changed · +69 −3
CHANGES.txt+9 −0 modified@@ -14,6 +14,15 @@ Security Fix (This fix was released in WebOb 1.8.8) +- The fix for CVE-2024-42353 was incomplete: a Location value containing + ASCII tab, carriage return, or line feed characters between consecutive + slashes could still be interpreted as a protocol-relative URL by + ``urllib.parse.urljoin`` on Python 3.10+, allowing an open redirect. + + See https://github.com/Pylons/webob/security/advisories/GHSA-fh3h-vg37-cc95 + + Thanks to Caleb Brown of Google for the report. + Feature ~~~~~~~
src/webob/response.py+8 −3 modified@@ -1356,12 +1356,17 @@ def md5_etag(self, body=None, set_content_md5=False): @staticmethod def _make_location_absolute(environ, value): + # urllib.parse.urlsplit() (called internally by urljoin) strips + # ASCII tab, CR, and LF from the URL on Python 3.10+. Strip them + # ourselves first so they cannot be used to bypass the SCHEME_RE + # or protocol-relative ("//") checks below. See CVE-2024-42353, + # https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3, + # and the follow-up advisory GHSA-fh3h-vg37-cc95. + value = value.replace("\t", "").replace("\r", "").replace("\n", "") + if SCHEME_RE.search(value): return value - # This is to fix an open redirect issue due to the way that - # urlparse.urljoin works. See CVE-2024-42353 and - # https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3 if value.startswith("//"): value = f"/%2f{value[2:]}" new_location = urlparse.urljoin(_request_uri(environ), value)
tests/test_response.py+52 −0 modified@@ -1096,6 +1096,58 @@ def test_location_no_open_redirect(): assert req.get_response(res).location == "http://localhost/%2fwww.example.com/test" +@pytest.mark.parametrize( + "payload", + [ + "/\t/www.example.com/test", + "\t//www.example.com/test", + "//\twww.example.com/test", + "/\t\t/www.example.com/test", + ], +) +def test_location_no_open_redirect_tab_bypass(payload): + # Follow-up to CVE-2024-42353. urllib.parse.urlsplit() (used internally + # by urljoin) strips ASCII tab on Python 3.10+, which allowed a + # Location value to bypass the "//" check and be parsed as + # protocol-relative. See GHSA-fh3h-vg37-cc95. (CR and LF are already + # rejected by the location header setter, so only tab is reachable + # via the public API.) + res = Response() + res.status = "301" + res.location = payload + req = Request.blank("/") + assert req.get_response(res).location == ( + "http://localhost/%2fwww.example.com/test" + ) + + +@pytest.mark.parametrize( + "payload", + [ + "/\t/www.example.com/test", + "/\n/www.example.com/test", + "/\r/www.example.com/test", + "\t//www.example.com/test", + "\n//www.example.com/test", + "\r//www.example.com/test", + "//\twww.example.com/test", + "//\nwww.example.com/test", + "//\rwww.example.com/test", + "//\tw\nww.example.com/test", + ], +) +def test__make_location_absolute_strips_url_whitespace(payload): + # Defense in depth for GHSA-fh3h-vg37-cc95: even when called with a + # Location value that bypasses the descriptor's CR/LF check (e.g. via + # direct manipulation of _headerlist), tab/CR/LF must not be usable to + # turn a relative path into a protocol-relative redirect. + result = Response._make_location_absolute( + {"wsgi.url_scheme": "http", "HTTP_HOST": "example.com:80"}, + payload, + ) + assert result == "http://example.com/%2fwww.example.com/test" + + @pytest.mark.xfail( sys.version_info < (3, 0), reason="Python 2.x unicode != str, WSGI requires str. Test "
1f681a4f17fcAdd fix for open redirect
2 files changed · +16 −0
src/webob/response.py+5 −0 modified@@ -1359,6 +1359,11 @@ def _make_location_absolute(environ, value): if SCHEME_RE.search(value): return value + # This is to fix an open redirect issue due to the way that + # urlparse.urljoin works. See CVE-2024-42353 and + # https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3 + if value.startswith("//"): + value = f"/%2f{value[2:]}" new_location = urlparse.urljoin(_request_uri(environ), value) return new_location
tests/test_response.py+11 −0 modified@@ -1085,6 +1085,17 @@ def test_location(): assert req.get_response(res).location == "http://localhost/test2.html" +def test_location_no_open_redirect(): + # This is a test for a fix for CVE-2024-42353 and + # https://github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3 + res = Response() + res.status = "301" + res.location = "//www.example.com/test" + assert res.location == "//www.example.com/test" + req = Request.blank("/") + assert req.get_response(res).location == "http://localhost/%2fwww.example.com/test" + + @pytest.mark.xfail( sys.version_info < (3, 0), reason="Python 2.x unicode != str, WSGI requires str. Test "
Vulnerability mechanics
Root cause
"Python 3.10+ urlsplit stripping whitespace characters allowed bypasses of previous security fixes for open redirect vulnerabilities."
Attack vector
An attacker can craft a `Location` header value containing whitespace characters like tab, carriage return, or newline between slashes. When WebOb normalizes this header, Python's `urllib.parse.urlsplit` (used internally by `urljoin`) strips these characters. This manipulation causes `urlsplit` to interpret the value as a protocol-relative URL, leading `urljoin` to use the attacker-controlled hostname instead of the original request hostname [ref_id=1]. This bypasses earlier protections against open redirects.
Affected code
The vulnerability lies within the `_make_location_absolute` static method in `src/webob/response.py`. This method is responsible for normalizing the `Location` header value before it is used in redirects. The fix involves modifying this method to strip whitespace characters before further processing [patch_id=4798104].
What the fix does
The patch addresses the bypass by explicitly removing tab, carriage return, and newline characters from the `Location` header value before it is processed by `urllib.parse.urlsplit` [patch_id=4798104]. This ensures that these whitespace characters cannot be used to create a protocol-relative URL and bypass the security checks. The fix prevents the `urljoin` function from incorrectly using an attacker-controlled hostname, thereby closing the open redirect vulnerability.
Preconditions
- inputThe application must use WebOb's `Response` class to set a `Location` header.
- inputThe `Location` header value must be crafted to include whitespace characters (tab, CR, LF) that can be stripped by `urllib.parse.urlsplit` on Python 3.10+.
Generated on Jun 4, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-fh3h-vg37-cc95ghsaADVISORY
- github.com/Pylons/webob/commit/1f681a4f17fc10777ef861e8b43ecb26053bc539ghsa
- github.com/Pylons/webob/commit/2b9fbedafb31180c910cf8526e9ea72b4603d0bcghsa
- github.com/Pylons/webob/releases/tag/1.8.10ghsa
- github.com/Pylons/webob/security/advisories/GHSA-fh3h-vg37-cc95ghsa
- github.com/Pylons/webob/security/advisories/GHSA-mg3v-6m49-jhp3ghsa
News mentions
0No linked articles in our index yet.