VYPR
Medium severity6.1NVD Advisory· Published Jun 4, 2026

WebOb: Location header normalization during redirect leads to open redirect - again

CVE-2026-44889

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

2

Patches

2
2b9fbedafb31

Fix open redirect issue due to changes made in cPython >=3.10

https://github.com/Pylons/webobDelta RegeerMay 6, 2026via ghsa-ref
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 "
    
1f681a4f17fc

Add fix for open redirect

https://github.com/Pylons/webobDelta RegeerAug 7, 2024via ghsa-ref
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

News mentions

0

No linked articles in our index yet.