mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(ci): veria — shell-quote PR-gate resolver output written to $BASH_ENV
The version-resolver step echoes the resolved CLAUDE_CODE_VERSION into $BASH_ENV unquoted; CircleCI sources $BASH_ENV at the start of every subsequent step *before* any env -i wrapper can run, so the job env (with provider credentials in scope) is live at that moment. A malicious PR could make the resolver — which lives under tests/claude_code/ and is therefore PR-controlled — print a value containing a newline + shell snippet to exfiltrate ANTHROPIC_API_KEY / AWS_* / VERTEXAI_* / AZURE_FOUNDRY_* / GITHUB_TOKEN. Two defenses: - Reject anything that isn't a strict `N.N.N` semver via `[[ ... =~ ^N.N.N$ ]]` (whole-string match, not per-line grep). - shell-quote on write via `printf 'export ...=%q\n'` so a bypass of the regex still cannot break out of the export assignment. Pin both with a structural unit test alongside the existing scrub pins.
This commit is contained in:
parent
5d121fa697
commit
16f2eba313
2 changed files with 54 additions and 1 deletions
|
|
@ -2355,8 +2355,22 @@ jobs:
|
|||
LC_ALL="${LC_ALL:-}" \
|
||||
TMPDIR="${TMPDIR:-/tmp}" \
|
||||
uv run --no-sync python -m tests.claude_code.pr_gate_version_resolver)
|
||||
# The resolver lives under tests/claude_code/ and is therefore
|
||||
# PR-controlled: a malicious PR could make it print a value
|
||||
# containing a newline + shell snippet. Reject anything that
|
||||
# is not a strict `N.N.N` semver before persisting, and
|
||||
# shell-quote on write so a bypass of the regex still cannot
|
||||
# break out of the `export` assignment when CircleCI sources
|
||||
# $BASH_ENV at the start of every subsequent step (which
|
||||
# happens before our `env -i` wrappers can run and would
|
||||
# otherwise execute the injected snippet with provider
|
||||
# credentials in scope).
|
||||
if ! [[ "$CLAUDE_CODE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "pr_gate_version_resolver returned a non-semver value; refusing to persist" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Selected @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
|
||||
echo "export CLAUDE_CODE_VERSION=${CLAUDE_CODE_VERSION}" >> "$BASH_ENV"
|
||||
printf 'export CLAUDE_CODE_VERSION=%q\n' "$CLAUDE_CODE_VERSION" >> "$BASH_ENV"
|
||||
- run:
|
||||
name: Install Node.js 20 + Claude Code CLI
|
||||
command: |
|
||||
|
|
|
|||
|
|
@ -286,6 +286,45 @@ def test_pr_gate_pytest_step_scrubs_secrets_from_env(
|
|||
)
|
||||
|
||||
|
||||
def test_pr_gate_resolver_output_safely_persisted_to_bash_env(
|
||||
circleci_config: dict,
|
||||
) -> None:
|
||||
"""The resolver step writes the resolved version to `$BASH_ENV` so
|
||||
later steps can interpolate it. `$BASH_ENV` is sourced by bash at
|
||||
the start of every subsequent step *before* any `env -i` wrapper
|
||||
we install can run, so the job env (with provider credentials in
|
||||
scope) is live at that moment. The resolver lives under
|
||||
`tests/claude_code/` and is therefore PR-controlled — a malicious
|
||||
PR could make it print a value containing a newline + shell
|
||||
snippet to exfiltrate credentials.
|
||||
|
||||
Pin the two defenses so they cannot silently regress:
|
||||
|
||||
1. The persisted value must be shell-quoted via `printf '%q'`
|
||||
(not unquoted via `echo`) so any bytes the resolver emits are
|
||||
safely re-parsed as a literal `export` assignment.
|
||||
2. The resolver output must be matched against a strict semver
|
||||
regex and rejected otherwise, so anything that isn't a
|
||||
`N.N.N` string never reaches `$BASH_ENV` in the first place.
|
||||
"""
|
||||
job = circleci_config["jobs"][JOB_NAME]
|
||||
command = _find_step_command(job, "Resolve Claude Code CLI version")
|
||||
assert command, "PR gate must have a step that resolves the CLI version."
|
||||
assert "printf 'export CLAUDE_CODE_VERSION=%q\\n'" in command, (
|
||||
"Resolver step must persist CLAUDE_CODE_VERSION via `printf '%q'` "
|
||||
"(shell-quoted) — a raw `echo \"export ...=$VAR\"` lets PR-controlled "
|
||||
"resolver output inject shell commands into $BASH_ENV that run with "
|
||||
"provider credentials in scope at the start of the next step."
|
||||
)
|
||||
assert "[[ \"$CLAUDE_CODE_VERSION\" =~ ^[0-9]+\\.[0-9]+\\.[0-9]+$ ]]" in command, (
|
||||
"Resolver step must validate CLAUDE_CODE_VERSION against a strict "
|
||||
"whole-string semver regex (`[[ ... =~ ^N.N.N$ ]]`) before "
|
||||
"persisting; a per-line grep would pass a multi-line resolver "
|
||||
"output, and anything that isn't a `N.N.N` string should never "
|
||||
"reach $BASH_ENV / `npm install`."
|
||||
)
|
||||
|
||||
|
||||
def test_existing_proxy_e2e_anthropic_job_unchanged(circleci_config: dict) -> None:
|
||||
"""No regression to the existing `proxy_e2e_anthropic_messages_tests`
|
||||
job (acceptance criterion). We don't lock its full body, but we do
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue