Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
MEDIUM severity

CVE-2026-54770 — webob

MEDIUMFix: Pylons/webob@ff89560

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

Also known asGHSA-6hx8-3wjj-gr8gPYSEC-2026-3943
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 12, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

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

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs35th percentile — riskier than 35% of all scored CVEsHighest risk
0.00%0.31%0.61%0.92%0.3%0.4%0.4%Sep 26Oct 26Oct 26

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

1 pkg affected
🐍webob

Real-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):

  1. The explicit strip only removes \t, \r, \n — the leading space survives.
  2. SCHEME_RE (^[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(_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 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): 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-influenced Location and serves the response (the classic redirect path).
  • Request.relative_url() — 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.path_url, self.location) without going through _make_location_absolute() 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

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 uses webob.util.urljoin().
  • Request.relative_url() now uses webob.util.urljoin().
  • webob.exc._HTTPMove now normalizes its Location through the same _make_location_absolute() 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

To report a vulnerability to the Pylons Project please take a look at:

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, 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 _make_location_absolute(), so a protocol-relative location such as //evil.example/path/ redirected off-host through that separate code path.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIweboball versions1.8.11pip install --upgrade 'webob==1.8.11'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    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.

  2. 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.

  3. 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.

Red HatImportant

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…

Workaround published by Red Hat
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)
ProductFixed inAdvisory
Red Hat Quay 3.10quay/quay-rhel8:1791462428RHSA-2026:79366
Red Hat Quay 3.12quay/quay-rhel8:1791314514RHSA-2026:79546
Red Hat Quay 3.16quay/quay-rhel9:1789563753RHSA-2026:69255
Red Hat Quay 3.17quay/quay-rhel9:1790690298RHSA-2026:74511
Red Hat Quay 3.9quay/quay-rhel8:1790808443RHSA-2026:77656

Frequently Asked Questions

## 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/L
O3 Security · Impact-Aware SCA

Is CVE-2026-54770 in your dependencies?

Find it across PyPI, including transitive dependencies.

CVE-2026-54770: webob Open Redirect — Fixed in 1.8.11