8bac460177
* fix: add concurrency protection to action sync workflows * style: fix formatting in action/modes.ts --------- Co-authored-by: pullfrog[bot] <226033991+pullfrog[bot]@users.noreply.github.com>
60 lines
2.4 KiB
TypeScript
60 lines
2.4 KiB
TypeScript
import type { AgentResult, TestRunnerOptions, ValidationCheck } from "../utils.ts";
|
|
import { defineFixture } 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 shell
|
|
* 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 (shell 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,
|
|
shell: "disabled",
|
|
effort: "auto",
|
|
timeout: "5m",
|
|
},
|
|
{ localOnly: true }
|
|
);
|
|
|
|
function validator(result: AgentResult): ValidationCheck[] {
|
|
const output = result.structuredOutput;
|
|
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 (shell 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"],
|
|
};
|