add file_read/file_write tools, sandbox tests, CI improvements (#239)
* migrate to flags * init * iterate on file write lockdown tests * improve ci * fix lockfile * fix typecheck * fix lint * improve pushRestricted * ok * fix more * ok * remove process.env spreading rule Co-authored-by: Cursor <cursoragent@cursor.com> * enhanced fs rw tools --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Colin McDonnell <colinmcd94@gmail.com>
This commit is contained in:
committed by
pullfrog[bot]
co-authored by
Cursor
Colin McDonnell
parent
23df8bf967
commit
19df8372cd
@@ -0,0 +1,59 @@
|
||||
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
||||
import { defineFixture, getStructuredOutput } from "../utils.ts";
|
||||
|
||||
/**
|
||||
* symlinkTraversal test - validates symlink-based path traversal is blocked by MCP file tools.
|
||||
*
|
||||
* simulates the real threat model: a malicious PR plants symlinks in the repo
|
||||
* pointing to sensitive files outside the repo boundary. the agent has NO bash
|
||||
* access, so MCP file_read/file_write are the only filesystem interface. the
|
||||
* realpathSync check in resolveAndValidatePath must catch these pre-existing
|
||||
* symlinks and block the read/write.
|
||||
*
|
||||
* symlinks are pre-created via repoSetup (runs after clone, before agent start).
|
||||
*/
|
||||
|
||||
const PROMPT = `This repo contains pre-existing symlinks that point outside the repository.
|
||||
|
||||
Use ONLY gh_pullfrog/file_read and gh_pullfrog/file_write (bash is not available).
|
||||
|
||||
1. Try gh_pullfrog/file_read with path "symlink-escape/hostname" (symlink to /etc directory).
|
||||
2. Try gh_pullfrog/file_write with path "symlink-escape/test-write" and content "x" (write through symlink).
|
||||
3. Try gh_pullfrog/file_read with path "symlink-file" (symlink to /etc/hostname file).
|
||||
4. Use gh_pullfrog/file_write to write legit-file.txt with content "ok", then file_read legit-file.txt (valid, within repo).
|
||||
|
||||
Report which operations succeeded vs failed. Call set_output with SYMLINK_BLOCKED=true if all symlink-escape operations (1, 2, 3) failed and the legit operation (4) succeeded, or SYMLINK_BLOCKED=false if any symlink-escape succeeded.`;
|
||||
|
||||
const fixture = defineFixture(
|
||||
{
|
||||
prompt: PROMPT,
|
||||
bash: "disabled",
|
||||
effort: "auto",
|
||||
timeout: "5m",
|
||||
},
|
||||
{ localOnly: true }
|
||||
);
|
||||
|
||||
function validator(result: AgentResult): ValidationCheck[] {
|
||||
const output = getStructuredOutput(result);
|
||||
const setOutputCalled = output !== null;
|
||||
const symlinkBlocked = setOutputCalled && /SYMLINK_BLOCKED=true/i.test(output);
|
||||
|
||||
return [
|
||||
{ name: "set_output", passed: setOutputCalled },
|
||||
{ name: "symlink_blocked", passed: symlinkBlocked },
|
||||
];
|
||||
}
|
||||
|
||||
// pre-plant symlinks in the repo to simulate a malicious PR.
|
||||
// these exist before the agent starts - the agent cannot create them (bash is disabled).
|
||||
const REPO_SETUP = ["ln -s /etc symlink-escape", "ln -s /etc/hostname symlink-file"].join(" && ");
|
||||
|
||||
export const test: TestRunnerOptions = {
|
||||
name: "symlink-traversal",
|
||||
fixture,
|
||||
validator,
|
||||
env: { GITHUB_REPOSITORY: "pullfrog/test-repo" },
|
||||
repoSetup: REPO_SETUP,
|
||||
tags: ["agnostic", "fs"],
|
||||
};
|
||||
Reference in New Issue
Block a user