fix(skill-evals): contain fixture reads inside the eval tree - #1315
Merged
Merged
Conversation
`magpie-run-evals.sh` is excluded from the sandbox, so everything the eval runner reads, it reads unsandboxed. Fixtures are deliberately left agent-writable, on the stated grounds that a fixture can only route *repository* text to the model — text the session could send anyway. Nothing enforced the "repository" half of that. There are two ways out of the tree, both reachable by a sandboxed agent writing a fixture: - A symlink. `report.md -> ~/.ssh/id_rsa` is followed by `read_text()` and its contents go into the prompt piped to `claude -p`. The wrapper's guard constrains how its argument is spelled, not where it leads, and `-d` follows a directory symlink just as happily. - A path string. `step-config.json` supplies `skill_md`, which is joined to the repository root and read with no containment, so enough `..` walks out with no symlink involved at all. Either turns a fixture read into a read of any file the host can see, including the credential paths the sandbox denies the session directly — which is the one thing the exclusion was not supposed to buy. Resolve and contain instead: the wrapper resolves its target against `tools/skill-evals/evals`, and the runner resolves every fixture path it reads. Containment is by resolved path rather than a ban on symlinks, so shared fixtures factored out inside the tree keep working. Found by an adversarial review pass over apache#1311. Generated-by: Claude Opus 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
magpie-run-evals.shis named insandbox.excludedCommands, so everything the eval runner reads, it reads unsandboxed. fix(sandbox): use Edit(path) for file denies, add the eval exclusion #1311 left fixtures agent-writable on an explicit, reasoned basis: a fixture "can route repository text to the model, which the session's own API access already allows."report.md -> ~/.ssh/id_rsais followed byread_text()and its contents land in the prompt piped toclaude -p. The wrapper's guard checks how its argument is spelled (tools/skill-evals/evalsprefix, no..), not where it leads — and-dfollows a directory symlink just as happily.step-config.jsonsuppliesskill_md, which is joined to the repo root and read with no containment, so enough..walks straight out.tools/skill-evals/evals; the runner resolves every fixture path it reads. Containment is by resolved path, not a ban on symlinks, so shared fixtures factored out inside the tree keep working.This is a follow-up to #1311 (merged), not a regression in it — the gap is in the accepted-risk reasoning, which this PR makes true by construction rather than by assumption. The comment block in the wrapper is updated to say what is now enforced instead of assumed.
Type of change
.claude/skills/<name>/) — eval fixtures updated belowtools/<system>/*.md)tools/*/withpyproject.toml) —tools/skill-evalsdocs/,README.md,CONTRIBUTING.md)projects/_template/)prek, workflows, validators)Test plan
prek run --all-filespasses (32 hooks, exit 0)uv run pytest tools/skill-evals/tests/— 170 passedload_casereturned the symlinked outside file's contents with no error — the escape is confirmed, not theoretical.load_step_configfollowed both a symlinkedsystem-prompt.mdand a..-escapingskill_md.test_load_case_allows_a_symlink_that_stays_inside_the_eval_treepins that this is containment, not a symlink ban, andtest_lets_a_real_path_inside_the_evals_tree_throughpins that the wrapper gate does not cost the normal case.tools/skill-evals/tests/test_run_evals_wrapper.py— the wrapper had no tests before; the three pre-existing gates (one argument, no flags, prefix) are now pinned alongside the new one.RFC-AI-0004 compliance
Linked issues
Refs #1311.
Notes for reviewers (optional)
test_load_case_refuses_a_fixture_symlinked_outside_the_eval_treereturned the planted secret rather than raising. Theskill_mdpath-traversal vector is a second hole in the same class that the original report did not name.load_casecontains againstcase_dir.parent(the fixtures dir, which by construction is the case's parent),load_step_configagainstfixtures_dir, andskill_mdagainst the repo root — since a legitimateskill_mdgenuinely points elsewhere in the repo. If you would rather have one root (the evals tree) threaded through all of them, say so; it is a slightly larger change to the call signatures.find_repo_root-relative reads elsewhere in the runner that are not fixture-controlled were left alone deliberately, to keep this to the trust boundary the exclusion created.🤖 Generated with Claude Code