Fail closed when the proxy/redirect path decode cap is exceeded

The decode-until-stable loops in _sanitize_proxy_path (terminals.py, cap 8) and
_safe_static_redirect_path (models.py, cap 2) proceed with whatever remains after the
cap instead of rejecting it. A path encoded more times than the cap therefore exits the
loop still percent-encoded, passes the literal '..' / prefix checks (the dots are still
%2E, not '..'), and is forwarded to the upstream terminal server, or emitted as a
redirect Location, which then decodes it once more and resolves the traversal. This is
the residual of the decode-until-stable hardening added for CVE-2026-54017: the cap is a
fixed depth, not a true stability guarantee.

Reject when the value is still not stable after the cap (unquote(x) != x), so anything
encoded more deeply than the cap fails closed rather than being forwarded. Legitimate
paths stabilize within a pass or two and are unaffected.

Co-authored-by: DavidCarliez <271374756+DavidCarliez@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Classic298 2026-06-14 22:49:13 +02:00
parent f85cb27ef8
commit 5f4bf0feee
2 changed files with 6 additions and 0 deletions

View file

@ -60,6 +60,9 @@ def _safe_static_redirect_path(url: str) -> str | None:
if decoded == path:
break
path = decoded
# Fail closed: a value still encoded after the cap would be decoded further downstream.
if unquote(path) != path:
return None
if '\x00' in path or '\\' in path:
return None
if not path.startswith('/'):

View file

@ -43,6 +43,9 @@ def _sanitize_proxy_path(path: str) -> str | None:
if once == decoded:
break
decoded = once
# Fail closed: still encoded after the cap means the upstream would decode further into traversal.
if unquote(decoded) != decoded:
return None
had_trailing_slash = decoded.endswith('/')
normalized = posixpath.normpath(decoded)
# Remove any leading slashes that would reset the base