diff --git a/CHANGELOG.md b/CHANGELOG.md index a6d634ff3a..85d293ecb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed - **Installer now installs `@gsd-build/sdk` automatically** so `gsd-sdk` lands on PATH. Resolves `command not found: gsd-sdk` errors that affected every `/gsd-*` command after a fresh install or `/gsd-update` to 1.36+. Adds `--no-sdk` to opt out and `--sdk` to force reinstall. Implements the `--sdk` flag that was previously documented in README but never wired up (#2385) +- **Prompt sanitization now neutralizes the full delimiter family it already classifies as injection** — `` tags, whitespace-padded delimiter tags, closing `[\/SYSTEM]` / `[\/INST]` markers, and closing `<>` markers no longer pass through `sanitizeForPrompt()`. Fixes a security gap where malicious commit messages or other user-controlled text could retain prompt-boundary markers despite being "sanitized" (#2394) ## [1.37.1] - 2026-04-17 diff --git a/get-shit-done/bin/lib/security.cjs b/get-shit-done/bin/lib/security.cjs index d09c0beee6..8ff4b35b96 100644 --- a/get-shit-done/bin/lib/security.cjs +++ b/get-shit-done/bin/lib/security.cjs @@ -163,7 +163,7 @@ const OBFUSCATION_PATTERN_ENTRIES = [ }, { pattern: /<\/?(system|human|assistant|user)\s*>/i, - message: 'Delimiter injection pattern: // tag detected', + message: 'Delimiter injection pattern: system/assistant/user-style tag detected', }, { pattern: /0x[0-9a-fA-F]{16,}/, @@ -245,14 +245,17 @@ function sanitizeForPrompt(text) { // Neutralize XML/HTML tags that mimic system boundaries // Replace < > with full-width equivalents to prevent tag interpretation // Note: is excluded — GSD uses it as legitimate prompt structure - sanitized = sanitized.replace(/<(\/?)(?:system|assistant|human)>/gi, - (_, slash) => `<${slash || ''}system-text>`); + sanitized = sanitized.replace( + /<(\/?)(system|assistant|human|user)\s*>/gi, + (_, slash, role) => `<${slash || ''}${String(role).toLowerCase()}-text>` + ); // Neutralize [SYSTEM] / [INST] markers - sanitized = sanitized.replace(/\[(SYSTEM|INST)\]/gi, '[$1-TEXT]'); + sanitized = sanitized.replace(/\[(\/?)(SYSTEM|INST)\]/gi, + (_, slash, marker) => `[${slash || ''}${marker.toUpperCase()}-TEXT]`); // Neutralize <> markers - sanitized = sanitized.replace(/<<\s*SYS\s*>>/gi, '«SYS-TEXT»'); + sanitized = sanitized.replace(/<<\s*\/?\s*SYS\s*>>/gi, '«SYS-TEXT»'); return sanitized; } diff --git a/tests/security.test.cjs b/tests/security.test.cjs index d5c29d5979..97999f3051 100644 --- a/tests/security.test.cjs +++ b/tests/security.test.cjs @@ -218,17 +218,29 @@ describe('sanitizeForPrompt', () => { assert.ok(!result.includes(''), `Result still has : ${result}`); }); + test('neutralizes tags including spaced delimiters', () => { + const input = 'Before override after'; + const result = sanitizeForPrompt(input); + assert.ok(!result.includes(': ${result}`); + assert.ok(!result.includes(': ${result}`); + assert.ok(result.includes('user-text'), `Result should preserve neutralized role name: ${result}`); + }); + test('neutralizes [SYSTEM] markers', () => { const input = 'Text [SYSTEM] override [/SYSTEM]'; const result = sanitizeForPrompt(input); assert.ok(!result.includes('[SYSTEM]')); assert.ok(result.includes('[SYSTEM-TEXT]')); + assert.ok(!result.includes('[/SYSTEM]')); + assert.ok(result.includes('[/SYSTEM-TEXT]')); }); test('neutralizes <> markers', () => { - const input = 'Text <> override'; + const input = 'Text <> override <>'; const result = sanitizeForPrompt(input); assert.ok(!result.includes('<>')); + assert.ok(!result.includes('<>')); + assert.equal((result.match(/«SYS-TEXT»/g) || []).length, 2); }); test('preserves normal text', () => {