fix(security): enforce one redirect credential policy

This commit is contained in:
kshitijk4poor 2026-07-11 11:29:31 +05:30 committed by kshitij
parent b8dd1bf3a5
commit 6e75ba7fa0
6 changed files with 448 additions and 139 deletions

View file

@ -18,6 +18,7 @@ from pathlib import Path
from typing import Any, NamedTuple, Optional
from hermes_cli import __version__ as _HERMES_VERSION
from hermes_cli.urllib_security import open_credentialed_url
# Identify ourselves so endpoints fronted by Cloudflare's Browser Integrity
# Check (error 1010) don't reject the default ``Python-urllib/*`` signature.
@ -29,43 +30,9 @@ COPILOT_EDITOR_VERSION = "vscode/1.104.1"
COPILOT_REASONING_EFFORTS_GPT5 = ["minimal", "low", "medium", "high"]
COPILOT_REASONING_EFFORTS_O_SERIES = ["low", "medium", "high"]
_CREDENTIAL_REDIRECT_HEADERS = {
"authorization",
"x-api-key",
"api-key",
"x-goog-api-key",
"cookie",
}
_ORIGINAL_URLOPEN = urllib.request.urlopen
class _StripCredentialRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Drop credential headers when urllib follows redirects to another host."""
def __init__(self, original_host: str):
self._original_host = original_host
def redirect_request(self, req, fp, code, msg, headers, newurl):
new_host = (urllib.parse.urlparse(newurl).hostname or "").lower()
if new_host and new_host != self._original_host:
clean_headers = {
name: value
for name, value in req.header_items()
if name.lower() not in _CREDENTIAL_REDIRECT_HEADERS
}
return urllib.request.Request(newurl, headers=clean_headers, method="GET")
return super().redirect_request(req, fp, code, msg, headers, newurl)
def _urlopen_model_catalog_request(req: urllib.request.Request, *, timeout: float):
if urllib.request.urlopen is not _ORIGINAL_URLOPEN:
return urllib.request.urlopen(req, timeout=timeout)
if not any(name.lower() in _CREDENTIAL_REDIRECT_HEADERS for name, _ in req.header_items()):
return urllib.request.urlopen(req, timeout=timeout)
original_host = (urllib.parse.urlparse(req.full_url).hostname or "").lower()
opener = urllib.request.build_opener(_StripCredentialRedirectHandler(original_host))
return opener.open(req, timeout=timeout)
"""Open catalog requests without forwarding headers across origins."""
return open_credentialed_url(req, timeout=timeout)
# Fallback OpenRouter snapshot used when the live catalog is unavailable.
@ -3154,15 +3121,13 @@ def ensure_lmstudio_model_loaded(
load_headers = dict(headers)
load_headers["Content-Type"] = "application/json"
try:
with urllib.request.urlopen(
urllib.request.Request(
server_root + "/api/v1/models/load",
data=body,
headers=load_headers,
method="POST",
),
timeout=timeout,
) as resp:
load_request = urllib.request.Request(
server_root + "/api/v1/models/load",
data=body,
headers=load_headers,
method="POST",
)
with _urlopen_model_catalog_request(load_request, timeout=timeout) as resp:
resp.read()
except Exception:
return None

View file

@ -0,0 +1,80 @@
"""Security policy for credential-bearing stdlib urllib requests."""
from __future__ import annotations
import urllib.parse
import urllib.request
from collections.abc import Callable, Iterable
from typing import Any
# Headers safe to forward to a different origin. Everything else is dropped:
# custom provider headers routinely carry credentials under arbitrary names.
_CROSS_ORIGIN_SAFE_HEADERS = frozenset({"accept", "user-agent"})
_DEFAULT_PORTS = {"http": 80, "https": 443}
def url_origin(url: str) -> tuple[str, str, int | None]:
"""Return a normalized (scheme, hostname, effective port) origin."""
parsed = urllib.parse.urlparse(url)
scheme = (parsed.scheme or "").lower()
try:
port = parsed.port
except ValueError:
port = None
return (
scheme,
(parsed.hostname or "").lower().rstrip("."),
port or _DEFAULT_PORTS.get(scheme),
)
class SafeCredentialRedirectHandler(urllib.request.HTTPRedirectHandler):
"""Preserve request headers only while redirects stay on one origin."""
def __init__(
self,
original_url: str,
*,
cross_origin_safe_headers: Iterable[str] = _CROSS_ORIGIN_SAFE_HEADERS,
) -> None:
self._original_origin = url_origin(original_url)
self._cross_origin_safe_headers = frozenset(
str(name).lower() for name in cross_origin_safe_headers
)
def redirect_request(self, req, fp, code, msg, headers, newurl):
# Let urllib enforce status/method semantics first (notably 307/308).
redirected = super().redirect_request(req, fp, code, msg, headers, newurl)
if redirected is None:
return None
resolved_url = urllib.parse.urljoin(req.full_url, newurl)
if url_origin(resolved_url) != self._original_origin:
# Use an allowlist rather than guessing credential header names.
# normalize_extra_headers permits arbitrary secret-bearing names.
for name, _value in list(redirected.header_items()):
if name.lower() not in self._cross_origin_safe_headers:
redirected.remove_header(name)
return redirected
def open_credentialed_url(
request: urllib.request.Request,
*,
timeout: float,
opener_factory: Callable[..., Any] = urllib.request.build_opener,
):
"""Open a request without forwarding credentials across origins.
``opener_factory`` is an explicit instrumentation/test seam. Security is
never disabled based on global ``urlopen`` identity.
"""
opener = opener_factory(SafeCredentialRedirectHandler(request.full_url))
return opener.open(request, timeout=timeout)
__all__ = [
"SafeCredentialRedirectHandler",
"open_credentialed_url",
"url_origin",
]