fix(xai-oauth): pin inference base_url to x.ai origin (#28952)

XAI_BASE_URL / HERMES_XAI_BASE_URL let users repoint the OAuth-authenticated
inference endpoint, but the env override was an unguarded credential-leak
vector: a tampered .env or hostile shell init setting
XAI_BASE_URL=https://attacker.example/v1 would silently ship the SuperGrok
OAuth bearer to a third party on every request.

Add _xai_validate_inference_base_url() that pins the host to x.ai or a
*.x.ai subdomain and rejects non-HTTPS. On rejection, fall back to the
default with a warning rather than raise — a bad env var should not
deadlock auth, but should never leak the bearer either.

Apply at all three sites that read the env override for xai-oauth:
- hermes_cli/auth.py resolve_xai_oauth_runtime_credentials (main path)
- hermes_cli/auth.py _xai_oauth_loopback_login (initial login)
- agent/auxiliary_client.py _resolve_xai_oauth_for_aux (aux client)

E2E validated against four scenarios: attacker.example, lookalike
api.x.ai.evil.com, http:// downgrade on api.x.ai, and legit custom.x.ai
subdomain (which still resolves correctly).

Discovered while comparing against the opencode-grok-auth plugin
(github.com/ysnock404/opencode-grok-auth), which highlighted the same
guard on the OpenCode side.
This commit is contained in:
Teknium 2026-05-19 14:51:21 -07:00 committed by GitHub
parent c9d5ef28bf
commit 64a9a199bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 200 additions and 12 deletions

View file

@ -3465,6 +3465,62 @@ def _xai_validate_oauth_endpoint(url: str, *, field: str) -> str:
return url
def _xai_validate_inference_base_url(value: str, *, fallback: str) -> str:
"""Refuse a non-xAI base_url for the OAuth-authenticated inference path.
The xAI Grok OAuth bearer is a high-value, long-lived credential tied to
the user's SuperGrok subscription. ``XAI_BASE_URL`` / ``HERMES_XAI_BASE_URL``
let users repoint the inference endpoint (handy for staging or a local
proxy), but the env override is also a credential-leak vector: a tampered
``.env`` or hostile shell init that sets
``XAI_BASE_URL=https://attacker.example/v1`` would ship the OAuth access
token to a third party on every request, silently.
Pin the inference origin to ``api.x.ai`` (or any ``*.x.ai`` subdomain xAI
may add). On rejection, fall back to the default and log a warning rather
than raise a bad env var should not deadlock authentication, but it
should also never leak the bearer.
``value`` is the already-stripped, trailing-slash-trimmed candidate from
env. Empty input returns ``fallback`` unchanged.
"""
candidate = (value or "").strip().rstrip("/")
if not candidate:
return fallback
try:
parsed = urlparse(candidate)
except Exception:
logger.warning(
"Ignoring malformed xAI base_url override %r; using %s instead.",
candidate, fallback,
)
return fallback
if parsed.scheme != "https":
logger.warning(
"Refusing non-HTTPS xAI base_url override %r (xai-oauth bearer would "
"be sent in cleartext); falling back to %s.",
candidate, fallback,
)
return fallback
host = (parsed.hostname or "").lower()
if not host:
logger.warning(
"Ignoring xAI base_url override %r with no hostname; using %s instead.",
candidate, fallback,
)
return fallback
if host != "x.ai" and not host.endswith(".x.ai"):
logger.warning(
"Refusing xAI base_url override %r — host %r is not on the xAI origin "
"(expected x.ai or a *.x.ai subdomain). The xai-oauth bearer is only "
"valid against xAI's inference API; sending it elsewhere would leak "
"the credential. Falling back to %s.",
candidate, host, fallback,
)
return fallback
return candidate
def _xai_oauth_discovery(timeout_seconds: float = 15.0) -> Dict[str, str]:
try:
response = httpx.get(
@ -3710,10 +3766,10 @@ def resolve_xai_oauth_runtime_credentials(
)
raise
base_url = (
base_url = _xai_validate_inference_base_url(
os.getenv("HERMES_XAI_BASE_URL", "").strip().rstrip("/")
or os.getenv("XAI_BASE_URL", "").strip().rstrip("/")
or DEFAULT_XAI_OAUTH_BASE_URL
or os.getenv("XAI_BASE_URL", "").strip().rstrip("/"),
fallback=DEFAULT_XAI_OAUTH_BASE_URL,
)
return {
"provider": "xai-oauth",
@ -6542,10 +6598,10 @@ def _xai_oauth_loopback_login(
code="xai_token_exchange_invalid",
)
base_url = (
base_url = _xai_validate_inference_base_url(
os.getenv("HERMES_XAI_BASE_URL", "").strip().rstrip("/")
or os.getenv("XAI_BASE_URL", "").strip().rstrip("/")
or DEFAULT_XAI_OAUTH_BASE_URL
or os.getenv("XAI_BASE_URL", "").strip().rstrip("/"),
fallback=DEFAULT_XAI_OAUTH_BASE_URL,
)
return {
"tokens": {