mirror of
https://github.com/mksglu/context-mode.git
synced 2026-10-02 12:25:33 +08:00
feat: allow curl/wget with silent file-output, block stdout floods (#166)
Algorithm: split chained commands, evaluate each segment independently. Allow when ALL conditions met: 1. Output to file (-o, --output, >, >>) 2. NOT stdout alias (-o -, /dev/stdout) 3. Silent flag present (-s/--silent for curl, -q/--quiet for wget) 4. No verbose/trace flags (-v, --verbose, --trace) Example allowed: curl -sLo /tmp/file.tar.gz https://url Example blocked: curl https://url (stdout flood) Example blocked: curl -o file url (no -s, progress bar floods stderr) 9 new tests covering allow/block/chain cases. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
af524eb5a7
commit
2731ca2527
+45
-7
@@ -180,14 +180,52 @@ export function routePreToolUse(toolName, toolInput, projectDir, platform) {
|
||||
// like `gh issue edit --body "text with curl in it"` (Issue #63).
|
||||
const stripped = stripQuotedContent(command);
|
||||
|
||||
// curl/wget → replace with echo redirect
|
||||
// curl/wget — allow silent file-output downloads, block stdout floods (#166).
|
||||
// Algorithm: split chained commands, evaluate each segment independently.
|
||||
if (/(^|\s|&&|\||\;)(curl|wget)\s/i.test(stripped)) {
|
||||
return {
|
||||
action: "modify",
|
||||
updatedInput: {
|
||||
command: `echo "context-mode: curl/wget blocked. You MUST use ${t("ctx_fetch_and_index")}(url, source) to fetch URLs, or ${t("ctx_execute")}(language, code) to run HTTP calls in sandbox. Do NOT retry with curl/wget."`,
|
||||
},
|
||||
};
|
||||
// Split on chain operators (&&, ||, ;) to evaluate each segment
|
||||
const segments = stripped.split(/\s*(?:&&|\|\||;)\s*/);
|
||||
const hasDangerousSegment = segments.some(seg => {
|
||||
const s = seg.trim();
|
||||
// Only evaluate segments that contain curl or wget
|
||||
if (!/(^|\s)(curl|wget)\s/i.test(s)) return false;
|
||||
|
||||
const isCurl = /\bcurl\b/i.test(s);
|
||||
const isWget = /\bwget\b/i.test(s);
|
||||
|
||||
// Check for file output flags
|
||||
const hasFileOutput = isCurl
|
||||
? /\s(-o|--output)\s/.test(s) || /\s*>\s*/.test(s) || /\s*>>\s*/.test(s)
|
||||
: /\s(-O|--output-document)\s/.test(s) || /\s*>\s*/.test(s) || /\s*>>\s*/.test(s);
|
||||
|
||||
if (!hasFileOutput) return true; // no file output → dangerous
|
||||
|
||||
// Stdout aliases: -o -, -o /dev/stdout, -O -
|
||||
if (isCurl && /\s(-o|--output)\s+(-|\/dev\/stdout)(\s|$)/.test(s)) return true;
|
||||
if (isWget && /\s(-O|--output-document)\s+(-|\/dev\/stdout)(\s|$)/.test(s)) return true;
|
||||
|
||||
// Verbose/trace flags flood stderr → context
|
||||
if (/\s(-v|--verbose|--trace|-D\s+-)\b/.test(s)) return true;
|
||||
|
||||
// Must be silent (curl: -s/--silent, wget: -q/--quiet) to prevent progress bar stderr flood
|
||||
const isSilent = isCurl
|
||||
? /\s-[a-zA-Z]*s|--silent/.test(s)
|
||||
: /\s-[a-zA-Z]*q|--quiet/.test(s);
|
||||
if (!isSilent) return true;
|
||||
|
||||
return false; // safe: silent + file output + no verbose + no stdout alias
|
||||
});
|
||||
|
||||
if (hasDangerousSegment) {
|
||||
return {
|
||||
action: "modify",
|
||||
updatedInput: {
|
||||
command: `echo "context-mode: curl/wget blocked. You MUST use ${t("ctx_fetch_and_index")}(url, source) to fetch URLs, or ${t("ctx_execute")}(language, code) to run HTTP calls in sandbox. Do NOT retry with curl/wget."`,
|
||||
},
|
||||
};
|
||||
}
|
||||
// All segments safe → allow through
|
||||
return null;
|
||||
}
|
||||
|
||||
// Inline HTTP detection: strip only heredocs (not quotes) so that
|
||||
|
||||
@@ -59,6 +59,75 @@ describe("routePreToolUse", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// ─── curl/wget file-output allow-list (#166) ────────────
|
||||
|
||||
it("allows curl -sLo file (silent + file output)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -sL https://example.com/file.tar.gz -o /tmp/file.tar.gz",
|
||||
});
|
||||
expect(result).toBeNull(); // null = allow through
|
||||
});
|
||||
|
||||
it("allows curl -s --output file", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -s --output /tmp/stripe.tar.gz https://github.com/stripe/stripe-cli/releases/download/v1.38.1/stripe.tar.gz",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("allows wget -q -O file (quiet + file output)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "wget -q -O /tmp/terraform.zip https://releases.hashicorp.com/terraform/1.0.0/terraform_1.0.0_linux_amd64.zip",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("allows curl -s > file (silent + shell redirect)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -s https://example.com/data.json > /tmp/data.json",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("blocks curl -o - (stdout alias)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -s -o - https://example.com",
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.action).toBe("modify");
|
||||
});
|
||||
|
||||
it("blocks curl -o file WITHOUT silent flag", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -L -o /tmp/file.tar.gz https://example.com/file.tar.gz",
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.action).toBe("modify");
|
||||
});
|
||||
|
||||
it("blocks curl -o file with --verbose", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -s --verbose -o /tmp/file.tar.gz https://example.com/file.tar.gz",
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.action).toBe("modify");
|
||||
});
|
||||
|
||||
it("blocks chained: curl -sLo file && curl url (second floods)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -sL -o /tmp/file.tar.gz https://example.com/a.tar.gz && curl https://example.com/api",
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
expect(result!.action).toBe("modify");
|
||||
});
|
||||
|
||||
it("allows chained: curl -sLo file && tar xzf file (both safe)", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: "curl -sL -o /tmp/file.tar.gz https://example.com/a.tar.gz && tar xzf /tmp/file.tar.gz -C /tmp",
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("denies inline fetch() with modify action", () => {
|
||||
const result = routePreToolUse("Bash", {
|
||||
command: 'node -e "fetch(\'https://api.example.com/data\')"',
|
||||
|
||||
Reference in New Issue
Block a user