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

GHSA-6hx8-3wjj-gr8g

MEDIUMFix: Pylons/webob@ff89560

GHSA-6hx8-3wjj-gr8g is a medium-severity (CVSS 6.1) Open Redirect vulnerability in webob. O3 Security confirms whether GHSA-6hx8-3wjj-gr8g is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

WebOb: Open redirect in Location header normalization via leading C0 control / space characters

Also known asCVE-2026-54770
Published
Aug 27, 2026
Updated
Aug 27, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 27, 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 GHSA-6hx8-3wjj-gr8g.

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+0000U+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.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. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update webob to 1.8.11 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-6hx8-3wjj-gr8g is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-6hx8-3wjj-gr8g is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-6hx8-3wjj-gr8g. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

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 GHSA-6hx8-3wjj-gr8g in your dependencies?

O3 detects GHSA-6hx8-3wjj-gr8g across PyPI dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-6hx8-3wjj-gr8g: webob Open Redirect… | O3 Security