CVE-2026-54770 is a medium-severity (CVSS 6.1) Open Redirect vulnerability in webob. A fix is available for webob — see the affected versions and patch details below.
WebOb: Open redirect in Location header normalization via leading C0 control / space characters
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-54770.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
CVE-2026-54770 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 385,987 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
webobReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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 (SCHEME_RE 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._make_location_absolute() (in src/webob/response.py) performed,
prior to the fix:
value = value.replace("\t", "").replace("\r", "").replace("\n", "")
if SCHEME_RE.search(value): # ^[a-z]+: -> already absolute, return as-is
return value
if value.startswith("//"): # neutralize protocol-relative URLs
value = f"/%2f{value[2:]}"
new_location = urlparse.urljoin(_request_uri(environ), value)
Consider the Location value " //www.example.com/test" (a single leading space):
- The explicit strip only removes
\t,\r,\n— the leading space survives. SCHEME_RE(^[a-z]+:) does not match — the value starts with a space.value.startswith("//")is False — the value starts with a space, not/. The//→/%2fneutralization is skipped.urllib.parse.urljoin(_request_uri(environ), " //www.example.com/test")then strips the leading space before parsing, sees//www.example.com/test, treats it as protocol-relative, and returnshttp://www.example.com/test.
The same bypass works with a value such as " https://www.example.com/test"
(leading space + a full scheme): SCHEME_RE 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-influencedLocationand serves the response (the classic redirect path).Request.relative_url()— usedurllib.parse.urljoin()directly and was subject to the same character stripping.webob.exc._HTTPMovesubclasses (HTTPMovedPermanently,HTTPFound,HTTPSeeOther,HTTPTemporaryRedirect,HTTPPermanentRedirect, etc.) — these built their absolute Location withurlparse.urljoin(req.path_url, self.location)without going through_make_location_absolute()at all, so they bypassed even the tab/CR/LF strip and the//→/%2fneutralization. A protocol-relative location passed to e.g.HTTPFound(location="//evil.example")redirected off-host.
Proof of Concept
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.get_response(res).location)
# Vulnerable (<= 1.8.10): http://www.example.com/test <-- open redirect
# Fixed: http://localhost/ //www.example.com/test
Absolute-URL variant:
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:
from webob import exc
environ = {
"wsgi.url_scheme": "http", "SERVER_NAME": "localhost",
"SERVER_PORT": "80", "REQUEST_METHOD": "HEAD", "PATH_INFO": "/",
}
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 redirect_uri
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._make_location_absolute()now useswebob.util.urljoin().Request.relative_url()now useswebob.util.urljoin().webob.exc._HTTPMovenow normalizes its Location through the same_make_location_absolute()code path asResponse, 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
Locationheader / 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.parseURL 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):
[email protected](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._make_location_absolute(): the 1.8.10 fix stripped only ASCII tab/CR/LF, buturllib.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._HTTPMoveredirect exceptions (HTTPFoundand friends) performed their ownurllib.parse.urljoin()normalization and never went through_make_location_absolute(), so a protocol-relative location such as//evil.example/path/redirected off-host through that separate code path.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | webob | all versions | 1.8.11pip install --upgrade 'webob==1.8.11' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for webob, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update webob to 1.8.11 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-54770 is resolved across your whole dependency graph.
Workarounds
Stop reflecting attacker-controlled destinations: resolve every redirect target against an allowlist of paths or hosts you own, prefer a server-side key over a full URL in the request, and reject absolute URLs entirely where the flow only ever needs a relative one.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
This is an open redirect vulnerability in the WebOb library. An unauthenticated attacker can craft a malicious URL that, when processed by applications utilizing WebOb for redirects, bypasses URL validation. This could lead to users being redirected to attacker-controlled sites, facilitating phishing attacks or the…
Applications utilizing the WebOb library should implement strict validation of redirect target URLs. Configure applications to only allow redirects to trusted, fully-qualified URIs or to strictly allowlist permitted redirect destinations. Reject any redirect target that does not begin with an expected trusted host or a validated relative path, ensuring no leading whitespace or control characters are present.Source: Red Hat security advisory for CVE-2026-54770 (CC BY 4.0)
| Product | Fixed in | Advisory |
|---|---|---|
| Red Hat Quay 3.10 | quay/quay-rhel8:1791462428 | RHSA-2026:79366 |
| Red Hat Quay 3.12 | quay/quay-rhel8:1791314514 | RHSA-2026:79546 |
| Red Hat Quay 3.16 | quay/quay-rhel9:1789563753 | RHSA-2026:69255 |
| Red Hat Quay 3.17 | quay/quay-rhel9:1790690298 | RHSA-2026:74511 |
| Red Hat Quay 3.9 | quay/quay-rhel8:1790808443 | RHSA-2026:77656 |
Frequently Asked Questions
Is CVE-2026-54770 in your dependencies?
Find it across PyPI, including transitive dependencies.