Kestrel
대시보드로 돌아가기
CVE-2026-54770MEDIUM· 6.1MITRENVDGHSA대응게시일: 2026. 08. 20.수정일: 2026. 08. 27.

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

Open-Redirect

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
6.1medium

이론적 심각도 점수

EPSS
0.3%상위 73.0%

30일 내 악용 확률 예측

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

악용 경로
공격 벡터네트워크
공격 복잡도낮음
필요 권한불필요
사용자 상호작용필요
범위변경
영향
기밀성 영향낮음
무결성 영향낮음
가용성 영향없음
버전별 점수
CVSS 3.16.1MODERATE
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

상세 설명

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:

bash
1value = value.replace("\t", "").replace("\r", "").replace("\n", "")
2
3if SCHEME_RE.search(value): # ^[a-z]+: -> already absolute, return as-is
4 return value
5
6if value.startswith("//"): # neutralize protocol-relative URLs
7 value = f"/%2f{value[2:]}"
8
9new_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

python
1from webob import Response
2from webob.request import Request
3
4res = Response()
5res.status = "301"
6res.location = " //www.example.com/test" # note the single leading space
7
8req = Request.blank("/") # request host is "localhost"
9print(req.get_response(res).location)
10# Vulnerable (<= 1.8.10): http://www.example.com/test <-- open redirect
11# Fixed: http://localhost/ //www.example.com/test

Absolute-URL variant:

bash
1res.location = " https://www.example.com/test"
2# Vulnerable: https://www.example.com/test <-- off-host
3# Fixed: http://localhost/ https://www.example.com/test

Via the HTTP exceptions:

python
1from webob import exc
2
3environ = {
4 "wsgi.url_scheme": "http", "SERVER_NAME": "localhost",
5 "SERVER_PORT": "80", "REQUEST_METHOD": "HEAD", "PATH_INFO": "/",
6}
7m = exc.HTTPFound(location="//www.example.com/test")
8m(environ, lambda *a, **k: None)
9print(m.location)
10# Vulnerable: //www.example.com/test <-- open redirect
11# 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.

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.