fix(test): check sensitive paths before lstat to fix macOS permission error (#3157)

On macOS, lstat("/etc/master.passwd") throws EACCES before the
sensitive-path pattern check runs. Move pattern matching before
filesystem calls so security errors are thrown consistently
regardless of filesystem permissions.

Fixes #3153

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-04-02 20:12:20 -07:00 committed by GitHub
parent 44940ceb5b
commit b99a16616f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 25 additions and 9 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.30.8",
"version": "0.30.9",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -657,14 +657,9 @@ export function validatePromptFilePath(filePath: string): void {
// Normalize the path to resolve .. and textual tricks
let resolved = resolve(filePath);
// Follow symlinks to validate the real target path, not the symlink name.
// Without this, a symlink like `innocent.txt -> ~/.ssh/id_rsa` would bypass
// sensitive path checks because the resolved string wouldn't match patterns.
if (existsSync(resolved)) {
resolved = realpathSync(resolved);
}
// Check against sensitive path patterns
// Check against sensitive path patterns BEFORE any filesystem calls.
// On macOS, lstat("/etc/master.passwd") throws EACCES before we can check
// the pattern, so we must validate the textual path first.
for (const { pattern, description } of SENSITIVE_PATH_PATTERNS) {
if (pattern.test(resolved)) {
throw new Error(
@ -677,6 +672,27 @@ export function validatePromptFilePath(filePath: string): void {
);
}
}
// Follow symlinks to validate the real target path, not the symlink name.
// Without this, a symlink like `innocent.txt -> ~/.ssh/id_rsa` would bypass
// sensitive path checks because the resolved string wouldn't match patterns.
if (existsSync(resolved)) {
resolved = realpathSync(resolved);
// Re-check after symlink resolution — the real path may be sensitive
for (const { pattern, description } of SENSITIVE_PATH_PATTERNS) {
if (pattern.test(resolved)) {
throw new Error(
`Security check failed: cannot use '${filePath}' as a prompt file.\n\n` +
`This path points to ${description}.\n` +
"Prompt contents are sent to the agent and may be logged or stored remotely.\n\n" +
"For security, use a plain text file instead:\n" +
` 1. Create a new file: echo "Your instructions here" > prompt.txt\n` +
" 2. Use it: spawn <agent> <cloud> --prompt-file prompt.txt",
);
}
}
}
}
/**