fix: harden bounded reads and redirect validation (#3671)

Assisted-by: OpenAI Codex (model: GPT-5, autonomous)
This commit is contained in:
Pascal THUET
2026-07-22 22:18:10 +02:00
committed by GitHub
parent c0f4cee25a
commit 3a7a8758f7
10 changed files with 557 additions and 143 deletions

View File

@@ -22,6 +22,16 @@ class _TokenResponseTooLarge(Exception):
"""Raised when an Azure AD token response exceeds the bounded read limit."""
def _extract_token(payload: object, key: str) -> str | None:
"""Return a normalized token from a JSON object, or None for other shapes."""
if not isinstance(payload, dict):
return None
token = payload.get(key)
if not isinstance(token, str):
return None
return token.strip() or None
class AzureDevOpsAuth(AuthProvider):
"""Azure DevOps authentication provider.
@@ -79,8 +89,7 @@ class AzureDevOpsAuth(AuthProvider):
if result.returncode != 0:
return None
payload = _json.loads(result.stdout)
token = payload.get("accessToken", "").strip()
return token or None
return _extract_token(payload, "accessToken")
except (
OSError,
subprocess.TimeoutExpired,
@@ -146,8 +155,7 @@ class AzureDevOpsAuth(AuthProvider):
label="Azure DevOps token response",
).decode("utf-8")
)
token = payload.get("access_token", "").strip()
return token or None
return _extract_token(payload, "access_token")
except (
urllib.error.URLError,
OSError,

View File

@@ -17,7 +17,7 @@ from fnmatch import fnmatch
from typing import Callable
from urllib.parse import urlparse
from .._download_security import is_https_or_localhost_http, is_loopback_url
from .._download_security import is_safe_download_redirect
from . import get_provider
from .config import AuthConfigEntry, _default_config_path, find_entries_for_url, load_auth_config
@@ -62,15 +62,11 @@ RedirectValidator = Callable[[str, str], None]
def _validate_strict_redirect(old_url: str, new_url: str) -> None:
target_is_allowed = is_https_or_localhost_http(new_url)
remote_to_http_loopback = (
urlparse(new_url).scheme == "http"
and not is_loopback_url(old_url)
)
if not target_is_allowed or remote_to_http_loopback:
if not is_safe_download_redirect(old_url, new_url):
raise urllib.error.URLError(
f"unsafe redirect to {new_url}: target must use HTTPS with a hostname, "
"or stay within localhost over HTTP (127.0.0.1, ::1)"
"must not enter a local target from a remote host, and may use HTTP only "
"within loopback (for example localhost, 127.0.0.1, ::1)"
)
@@ -95,6 +91,8 @@ class _StripAuthOnRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
try:
new_parsed = urlparse(newurl)
# Force urllib's syntax and range validation before following.
new_parsed.port
except ValueError as exc:
# Malformed redirect target (e.g. unterminated IPv6 bracket).
# Surface as URLError so callers' download error handling applies.
@@ -177,9 +175,11 @@ def open_url(
*redirect_validator*, when provided, is called with ``(old_url, new_url)``
before following each redirect and may raise to reject the redirect.
Every attempt uses an isolated opener so a process-wide opener installed
with ``urllib.request.install_opener`` cannot replace the redirect guard.
Redirect scheme safety: every attempt goes through
``_StripAuthOnRedirect``, which rejects redirects to non-HTTPS URLs except
HTTP between localhost / 127.0.0.1 / ::1 URLs.
HTTP between loopback URLs, and rejects remote-to-local redirects.
"""
entries = find_entries_for_url(url, _load_config())