diff --git a/packages/core/src/permissions/classifier.ts b/packages/core/src/permissions/classifier.ts index b1d86fb8d..9b1197bd5 100644 --- a/packages/core/src/permissions/classifier.ts +++ b/packages/core/src/permissions/classifier.ts @@ -251,11 +251,23 @@ export async function classifyAction( */ export function sanitizeClassifierReason(raw: string): string { if (!raw) return raw; + + // Drop `<...>` pseudo-tags ("...", "...") that could be + // parsed as control fences by the main model's prompt. + // + // Replace iteratively until the string stabilises. A single `/g` pass + // can leave residual `<>` if the input was crafted to overlap (CodeQL + // 223). Bounded by a small iteration cap so the loop is always O(n) + // regardless of how the attacker structures the string. + let stripped = raw; + for (let i = 0; i < 8; i++) { + const next = stripped.replace(/<[^>]*>/g, ''); + if (next === stripped) break; + stripped = next; + } + return ( - raw - // Drop `<...>` pseudo-tags ("...", "...") that could - // be parsed as control fences by the main model's prompt. - .replace(/<[^>]*>/g, '') + stripped // Collapse newlines / runs of whitespace — defeats multi-paragraph // attempts to stage a fake "instruction block". .replace(/\s+/g, ' ') diff --git a/packages/core/src/permissions/dangerousRules.ts b/packages/core/src/permissions/dangerousRules.ts index dcc2d45f9..84722d716 100644 --- a/packages/core/src/permissions/dangerousRules.ts +++ b/packages/core/src/permissions/dangerousRules.ts @@ -89,8 +89,15 @@ const SHELL_LIKE_TOOLS: readonly string[] = Object.freeze([ */ function isInterpreterToken(rawToken: string): boolean { if (!rawToken) return false; - // Strip trailing wildcards / colons / arguments after `:` - const noWildcard = rawToken.replace(/[*]+$/, ''); + // Strip trailing wildcards. Using a manual loop instead of `/[*]+$/` + // both because the regex form trips CodeQL's polynomial-regex + // heuristic (CodeQL 222) and because end-of-string trim is O(n) by + // construction here. + let end = rawToken.length; + while (end > 0 && rawToken.charCodeAt(end - 1) === 42 /* '*' */) { + end--; + } + const noWildcard = rawToken.slice(0, end); const beforeColon = noWildcard.split(':')[0]; // Last path segment so `/usr/bin/python3` → `python3` const lastSegment = (beforeColon ?? '').split('/').pop() ?? '';