Skip to content

Commit 2576ed2

Browse files
committed
fix(security): block output write on critical match; tighten false-positive filter
Assisted-By: docker-agent
1 parent 0577823 commit 2576ed2

2 files changed

Lines changed: 41 additions & 8 deletions

File tree

src/security/__tests__/security.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* bash test name verbatim so results are easy to correlate.
77
*/
88

9-
import { readFileSync } from 'node:fs';
9+
import { existsSync, readFileSync } from 'node:fs';
1010
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
1111
import { tmpdir } from 'node:os';
1212
import { join } from 'node:path';
@@ -266,6 +266,34 @@ describe('test-security.sh: sanitize-input', () => {
266266
expect(outputContent).toMatch(/review this pull request/i);
267267
expect(outputContent).toMatch(/memory leaks/i);
268268
});
269+
270+
it('Fix A: output file not written to disk when CRITICAL pattern blocks', async () => {
271+
// Regression test for the bug where writeFileSync ran before the blocked
272+
// check, flushing tainted content to the output path even on block.
273+
const input = await writeInput('critical-no-write.txt', 'echo $ANTHROPIC_API_KEY\n');
274+
const out = outputPath('critical-no-write-out.txt');
275+
276+
const result = sanitizeInput(input, out);
277+
278+
expect(result.blocked).toBe(true);
279+
// The output file must NOT exist — tainted content must not land on disk.
280+
expect(existsSync(out)).toBe(false);
281+
});
282+
283+
it('Fix B: quoted CRITICAL-pattern line still detected (no metacharacters inside quotes)', async () => {
284+
// Before Fix B, isFalsePositive() matched the broad quoted-line regex for
285+
// +"echo $ANTHROPIC_API_KEY" (starts with +, content wrapped in "),
286+
// silently passing the exfiltration command through undetected.
287+
// After Fix B the quoted-line suppression requires regex metacharacters
288+
// inside the quotes; a plain shell command has none, so it IS detected.
289+
const input = await writeInput('quoted-critical.diff', '+"echo $ANTHROPIC_API_KEY"\n');
290+
const out = outputPath('quoted-critical-out.diff');
291+
292+
const result = sanitizeInput(input, out);
293+
294+
expect(result.blocked).toBe(true);
295+
expect(result.riskLevel).toBe('high');
296+
});
269297
});
270298

271299
describe('test-security.sh: sanitize-output', () => {

src/security/sanitize-input.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ function isFalsePositive(line: string): boolean {
2525
) {
2626
return true;
2727
}
28-
// Skip purely quoted lines — regex pattern definitions in code.
29-
// Matches lines that start with +, whitespace, or - and are entirely
30-
// a quoted string: e.g. '+"echo.*\\$.*KEY"' or " 'ignore.*previous'"
31-
if (/^[+\s-]\s*['"].*['"]\s*$/.test(line)) {
28+
// Skip purely quoted lines — but ONLY if the quoted content contains regex
29+
// metacharacters, indicating it is a pattern definition in code rather than
30+
// a real injection payload. A line like +"echo $ANTHROPIC_API_KEY" is NOT
31+
// a false positive: it has no bracket/brace/quantifier metacharacters. A
32+
// line like +'ghs_[a-zA-Z0-9]{36}' IS a false positive (contains [ ] { }).
33+
// Note: $ is intentionally excluded — shell variable references ($KEY)
34+
// must not suppress detection of exfiltration commands.
35+
if (/^[+\s-]\s*['"].*[[\]{}()*+?^\\].*['"]\s*$/.test(line)) {
3236
return true;
3337
}
3438
return false;
@@ -102,9 +106,6 @@ export function sanitizeInput(inputPath: string, outputPath: string): SanitizeIn
102106
}
103107
}
104108

105-
// Write sanitized output
106-
writeFileSync(outputPath, outputLines.join('\n'), 'utf-8');
107-
108109
// ── Determine outcome ───────────────────────────────────────────────────
109110
if (foundCritical) {
110111
core.error(
@@ -118,6 +119,10 @@ export function sanitizeInput(inputPath: string, outputPath: string): SanitizeIn
118119
return { blocked: true, stripped: false, riskLevel: 'high' };
119120
}
120121

122+
// Write sanitized output — only reached when not blocked.
123+
// Tainted content must never be flushed to disk before exit.
124+
writeFileSync(outputPath, outputLines.join('\n'), 'utf-8');
125+
121126
if (foundSuspicious) {
122127
core.info('⚠️ Input sanitization completed - suspicious content stripped from prompt');
123128
core.info(' Stripped lines will not be passed to the agent');

0 commit comments

Comments
 (0)