generated from coulomb/repo-seed
Fix warden access proxy for catalog fetch commands with shell pipes.
resolve_fetch_command used shlex.split, which treated `|` as a literal argument — breaking reuse-surface-hub-write-token (kubectl | base64 -d). Piped commands now run via shell=True with inherited stdio for --fetch.
This commit is contained in:
@@ -964,7 +964,7 @@ def _access_proxy(
|
||||
err.print("[yellow]Proxying ungated[/yellow] (--no-policy; gate not enforced).")
|
||||
|
||||
try:
|
||||
argv = resolve_fetch_command(entry, domain=domain, field=field, path=path)
|
||||
resolved = resolve_fetch_command(entry, domain=domain, field=field, path=path)
|
||||
except ProxyError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(2)
|
||||
@@ -979,9 +979,9 @@ def _access_proxy(
|
||||
if not child_argv:
|
||||
err.print("[red]--exec needs a command after `--`[/red], e.g. `-- npm publish`.")
|
||||
raise typer.Exit(2)
|
||||
rc = proxy_exec(argv, env_var=field or "", child_argv=child_argv)
|
||||
rc = proxy_exec(resolved, env_var=field or "", child_argv=child_argv)
|
||||
else:
|
||||
rc = proxy_fetch(argv)
|
||||
rc = proxy_fetch(resolved)
|
||||
except ProxyError as e:
|
||||
err.print(f"[red]{e}[/red]")
|
||||
raise typer.Exit(5)
|
||||
|
||||
@@ -25,6 +25,7 @@ import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
@@ -34,18 +35,43 @@ from warden.routing.models import RouteEntry
|
||||
_PLACEHOLDER = re.compile(r"<[^>]+>")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResolvedFetch:
|
||||
"""A catalog fetch command ready to run — either argv or a shell pipeline."""
|
||||
|
||||
argv: Optional[List[str]] = None
|
||||
shell_cmd: Optional[str] = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if bool(self.argv) == bool(self.shell_cmd):
|
||||
raise ValueError("exactly one of argv or shell_cmd must be set")
|
||||
|
||||
|
||||
class ProxyError(Exception):
|
||||
"""Raised when a proxy fetch cannot be performed safely."""
|
||||
|
||||
|
||||
def _has_shell_pipe(cmd: str) -> bool:
|
||||
"""True when ``cmd`` contains an unquoted shell pipe operator."""
|
||||
in_single = in_double = False
|
||||
for ch in cmd:
|
||||
if ch == "'" and not in_double:
|
||||
in_single = not in_single
|
||||
elif ch == '"' and not in_single:
|
||||
in_double = not in_double
|
||||
elif ch == "|" and not in_single and not in_double:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def resolve_fetch_command(
|
||||
entry: RouteEntry,
|
||||
*,
|
||||
domain: Optional[str] = None,
|
||||
field: Optional[str] = None,
|
||||
path: Optional[str] = None,
|
||||
) -> List[str]:
|
||||
"""Build the concrete argv for an entry's fetch, or raise if under-specified.
|
||||
) -> ResolvedFetch:
|
||||
"""Build the concrete fetch command for an entry, or raise if under-specified.
|
||||
|
||||
Starts from the catalog ``fetch_command`` template (with ``<path_template>``
|
||||
inlined), substitutes ``<domain>``/``<FIELD>`` and an explicit ``--path`` override,
|
||||
@@ -81,7 +107,10 @@ def resolve_fetch_command(
|
||||
"Supply --domain/--field (and --path for owner-side names) — warden will not "
|
||||
"guess owner-confirmed resource names."
|
||||
)
|
||||
return shlex.split(cmd)
|
||||
if _has_shell_pipe(cmd):
|
||||
# Catalog-reviewed pipelines (e.g. kubectl | base64 -d) need a shell.
|
||||
return ResolvedFetch(shell_cmd=cmd)
|
||||
return ResolvedFetch(argv=shlex.split(cmd))
|
||||
|
||||
|
||||
def caller_auth_present(token_envs: tuple[str, ...] = ("VAULT_TOKEN", "BAO_TOKEN")) -> bool:
|
||||
@@ -146,25 +175,37 @@ def _caller_env() -> dict:
|
||||
return dict(os.environ)
|
||||
|
||||
|
||||
def proxy_fetch(argv: List[str]) -> int:
|
||||
def proxy_fetch(resolved: ResolvedFetch) -> int:
|
||||
"""Run the owner's tool, streaming its output straight to the caller.
|
||||
|
||||
stdout/stderr are **inherited** (``None``), never piped — the secret value flows
|
||||
subsystem → caller and is never read into warden's memory, buffer, or log (G2).
|
||||
Returns the tool's exit code.
|
||||
"""
|
||||
completed = subprocess.run( # noqa: S603 — argv is shlex-split from a validated template
|
||||
argv,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
stdin=None,
|
||||
env=_caller_env(),
|
||||
check=False,
|
||||
)
|
||||
env = _caller_env()
|
||||
if resolved.argv is not None:
|
||||
completed = subprocess.run( # noqa: S603 — argv is shlex-split from a validated template
|
||||
resolved.argv,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
stdin=None,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
else:
|
||||
completed = subprocess.run( # noqa: S602 — shell_cmd is catalog-reviewed, not user input
|
||||
resolved.shell_cmd,
|
||||
shell=True,
|
||||
stdout=None,
|
||||
stderr=None,
|
||||
stdin=None,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
return completed.returncode
|
||||
|
||||
|
||||
def proxy_exec(argv: List[str], *, env_var: str, child_argv: List[str]) -> int:
|
||||
def proxy_exec(resolved: ResolvedFetch, *, env_var: str, child_argv: List[str]) -> int:
|
||||
"""Fetch the value and inject it into a child command's environment only.
|
||||
|
||||
The value transits warden's memory here (the accepted proxy tradeoff for `--exec`)
|
||||
@@ -175,10 +216,28 @@ def proxy_exec(argv: List[str], *, env_var: str, child_argv: List[str]) -> int:
|
||||
if not env_var:
|
||||
raise ProxyError("--exec requires --field (the env var name to inject), e.g. NPM_AUTH_TOKEN")
|
||||
|
||||
fetched = subprocess.run( # noqa: S603
|
||||
argv, stdout=subprocess.PIPE, stderr=None, stdin=None,
|
||||
env=_caller_env(), check=False, text=True,
|
||||
)
|
||||
env = _caller_env()
|
||||
if resolved.argv is not None:
|
||||
fetched = subprocess.run( # noqa: S603
|
||||
resolved.argv,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=None,
|
||||
stdin=None,
|
||||
env=env,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
else:
|
||||
fetched = subprocess.run( # noqa: S602
|
||||
resolved.shell_cmd,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=None,
|
||||
stdin=None,
|
||||
env=env,
|
||||
check=False,
|
||||
text=True,
|
||||
)
|
||||
if fetched.returncode != 0:
|
||||
raise ProxyError(
|
||||
f"fetch failed (exit {fetched.returncode}) — check caller auth and the path."
|
||||
|
||||
Reference in New Issue
Block a user