GHSA-6hx8-3wjj-gr8g: Medium severity pip/webob vulnerability
Summary
This is a third follow-up to CVE-2024-42353 / GHSA-mg3v-6m49-jhp3 and CVE-2026-44889 / GHSA-fh3h-vg37-cc95.
WebOb makes the Location header absolute when it serves a redirect. To stop a relative or protocol-relative target from redirecting users off-host, it checks the value for a URI scheme and for a leading //, then joins it against the request URI with urllib.parse.urljoin(). The previous fix additionally stripped ASCII tab/CR/LF from the value before those checks.
However, on Python 3.10+ urllib.parse.urljoin() (via urlsplit()) does more than remove tab/CR/LF: it also strips leading and trailing C0 control characters (U+0000–U+001F) and spaces from the URL before parsing it. Because WebOb's guard checks (SCHEMERE and startswith("//")) run against the un-stripped value, a single leading space or control byte slips past them, and urljoin() then silently removes that byte and parses what remains as a protocol-relative — or even absolute — URL. The result is an open redirect to an attacker-controlled host.
Details
Response.makelocationabsolute() (in src/webob/response.py) performed, prior to the fix:
python value = value.replace("\t", "").replace("\r", "").replace("\n", "")
if SCHEMERE.search(value): # ^[a-z]+: -> already absolute, return as-is return value
if value.startswith("//"): # neutralize protocol-relative URLs value = f"/%2f{value[2:]}"
newlocation = urlparse.urljoin(requesturi(environ), value)
Consider the Location value " //www.example.com/test" (a single leading space):
1. The explicit strip only removes \t, \r, \n — the leading space survives. 2. SCHEMERE (^[a-z]+:) does not match — the value starts with a space. 3. value.startswith("//") is False — the value starts with a space, not /. The // → /%2f neutralization is skipped. 4. urllib.parse.urljoin(requesturi(environ), " //www.example.com/test") then strips the leading space before parsing, sees //www.example.com/test, treats it as protocol-relative, and returns http://www.example.com/test.
The same bypass works with a value such as " https://www.example.com/test" (leading space + a full scheme): SCHEMERE does not match the space-prefixed string, but urljoin() strips the space and returns the fully absolute attacker URL https://www.example.com/test.
Any C0 control character works equally well in place of the space, e.g. "\x00//www.example.com/test" or "\x1f//www.example.com/test", because urlsplit() strips the whole leading C0-control-and-space run.
Affected entry points
- Response.location — any application that sets a relative/attacker-influenced Location and serves the response (the classic redirect path). - Request.relativeurl() — used urllib.parse.urljoin() directly and was subject to the same character stripping. - webob.exc.HTTPMove subclasses (HTTPMovedPermanently, HTTPFound, HTTPSeeOther, HTTPTemporaryRedirect, HTTPPermanentRedirect, etc.) — these built their absolute Location with urlparse.urljoin(req.pathurl, self.location) without going through makelocationabsolute() at all, so they bypassed even the tab/CR/LF strip and the // → /%2f neutralization. A protocol-relative location passed to e.g. HTTPFound(location="//evil.example") redirected off-host.
Proof of Concept
python from webob import Response from webob.request import Request
res = Response() res.status = "301" res.location = " //www.example.com/test" # note the single leading space
req = Request.blank("/") # request host is "localhost" print(req.getresponse(res).location) Vulnerable (<= 1.8.10): http://www.example.com/test <-- open redirect Fixed: http://localhost/ //www.example.com/test
Absolute-URL variant:
python res.location = " https://www.example.com/test" Vulnerable: https://www.example.com/test <-- off-host Fixed: http://localhost/ https://www.example.com/test
Via the HTTP exceptions:
python from webob import exc
environ = { "wsgi.urlscheme": "http", "SERVERNAME": "localhost", "SERVERPORT": "80", "REQUESTMETHOD": "HEAD", "PATHINFO": "/", } m = exc.HTTPFound(location="//www.example.com/test") m(environ, lambda a, k: None) print(m.location) Vulnerable: //www.example.com/test <-- open redirect Fixed: http://localhost/%2fwww.example.com/test
Impact
An unauthenticated remote attacker who controls (in whole or part) the redirect target of an application built on WebOb can redirect a user from a trusted host to an attacker-controlled host. This enables phishing and credential-theft campaigns that abuse the trusted origin, and can be chained with OAuth/SSO redirecturi flows to leak tokens. Exploitation requires user interaction (following the redirect). Confidentiality and integrity impact are limited (L); the scope is changed (C) because the trust boundary of the originating site is crossed.
Patches
Fixed by replacing the use of urllib.parse.urljoin() with WebOb's own RFC 3986 reference-resolution implementation, webob.util.urljoin(), which resolves the reference exactly as given, character for character, with no whitespace or control-character removal.
- Response.makelocationabsolute() now uses webob.util.urljoin(). - Request.relativeurl() now uses webob.util.urljoin(). - webob.exc.HTTPMove now normalizes its Location through the same makelocationabsolute() code path as Response, so protocol-relative and whitespace-smuggled locations are neutralized there too.
Users should upgrade to the patched release. There are no API changes.
Workarounds
- Only ever set the Location header / redirect target to a fully-qualified URI whose host you control, or strictly allowlist redirect destinations before handing them to WebOb. - Reject any redirect target that does not begin with https://yourhost/ (or a validated relative path with no leading whitespace/control bytes).
References
- This advisory: GHSA-6hx8-3wjj-gr8g - GHSA-fh3h-vg37-cc95 (CVE-2026-44889) — second incomplete fix (tab/CR/LF) - GHSA-mg3v-6m49-jhp3 (CVE-2024-42353) — original open redirect fix - RFC 3986, Section 5 — Reference Resolution: https://www.rfc-editor.org/rfc/rfc3986#section-5 - Python urllib.parse URL stripping behavior (CPython 3.10+, removal of leading and trailing C0 control and space characters): https://docs.python.org/3/library/urllib.parse.html To report a vulnerability to the Pylons Project please take a look at:
- Pylons Project security policy and reporting process: https://github.com/Pylons/.github/blob/main/SECURITY.md - Security contact (private, coordinated disclosure): pylons-project-security@googlegroups.com (the Pylons Project requests a 90-day disclosure embargo)
Credit
Reported via the Pylons Project security mailing list by:
- tonghuaroot — for the residual open redirect in Response.makelocationabsolute(): the 1.8.10 fix stripped only ASCII tab/CR/LF, but urllib.parse.urljoin() also strips leading C0 control and space characters, so values such as " //attacker.example/path" (and " https://attacker.example/path") still escaped off-host. - Matheus Polkorny — for identifying that the webob.exc.HTTPMove redirect exceptions (HTTPFound and friends) performed their own urllib.parse.urljoin() normalization and never went through makelocationabsolute(), so a protocol-relative location such as //evil.example/path/ redirected off-host through that separate code path.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/webobto a version that resolves this vulnerability.Fixed in 1.8.11 - Upgrade
Upgrade
WebObto a version that resolves this vulnerability.Fixed in 1.8.10 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Patch GHSA-mg3v-6m49-jhp3 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Patch GHSA-fh3h-vg37-cc95 - Configuration
For WebOb redirect exceptions (webob.exc._HTTPMove: HTTPFound, HTTPMovedPermanently, etc.) and Response location handling, replace the use of urllib.parse.urljoin() with webob.util.urljoin(), and ensure Response._make_location_absolute() and webob.exc._HTTPMove normalize the Location through the same code path, including stripping ASCII tab/CR/LF and leading C0 control characters and spaces before any protocol/host checks.
WebOb (redirect Location handling) Location normalization/absolute URL resolution = Use webob.util.urljoin() and normalize Location via the same checks; strip leading/trailing C0 control characters and space before parsing; apply tab/CR/LF stripping before checks - Configuration
Only ever set the Location/redirect target to a fully-qualified URI that begins with the allowed/trusted origin pattern specified by the advisory (the text states: reject redirect targets that do not begin with `https://yourhost/`).
Application redirect generation using WebOb Redirect target (Location header / redirect location) = Fully-qualified URI with explicit trusted origin (example constraint)
Event History
Frequently Asked Questions
Which deployments are affected?
The issue applies to WebOb redirect handling when running on Python 3.10 or later. It is relevant where an attacker can influence a redirect target used in a Location header.
What does an attacker need to exploit this?
An attacker needs to supply a redirect value with a leading space or C0 control character followed by a protocol-relative or absolute attacker-controlled URL. WebOb's checks evaluate the unstripped value, while Python's URL parsing strips the leading character before resolving the URL.
Does exploitation require an authenticated account?
No privileges are required according to the supplied severity vector. Exploitation does require user interaction, such as a user following the resulting redirect.
What is the likely impact?
A successful exploit can redirect users to an attacker-controlled host. The supplied severity vector rates confidentiality and integrity impact as low and availability impact as none.