|
1 | 1 | // Sanitizes command text before it is displayed in approval prompts. |
2 | | -import { redactSensitiveText, resolveRedactOptions } from "../logging/redact.js"; |
| 2 | +import { |
| 3 | + computeSensitiveRedactionBitmap, |
| 4 | + redactSensitiveText, |
| 5 | + resolveRedactOptions, |
| 6 | +} from "../logging/redact.js"; |
3 | 7 | import type { ExecApprovalRequestPayload } from "./exec-approvals.js"; |
4 | 8 |
|
5 | 9 | // Escape control characters, Unicode format/line/paragraph separators, and non-ASCII space |
@@ -60,31 +64,6 @@ function truncateForDisplay(text: string): SanitizedExecApprovalDisplayText { |
60 | 64 | }; |
61 | 65 | } |
62 | 66 |
|
63 | | -// Build a boolean bitmap of positions in `text` that ANY redaction pattern would match. |
64 | | -// Patterns are applied independently to the raw text (not sequentially against a |
65 | | -// progressively-redacted view) so later patterns can still find matches that the in-place |
66 | | -// redaction would have replaced first. That is conservative — it may over-count overlapping |
67 | | -// matches — but that is acceptable for a coverage check. Indices are UTF-16 code-unit |
68 | | -// offsets, matching what `matchAll` returns and aligning with `String#length`. |
69 | | -function computeRedactionBitmap(text: string, patterns: RegExp[]): boolean[] { |
70 | | - const bitmap: boolean[] = Array.from({ length: text.length }, () => false); |
71 | | - for (const pattern of patterns) { |
72 | | - const iter = pattern.flags.includes("g") |
73 | | - ? new RegExp(pattern.source, pattern.flags) |
74 | | - : new RegExp(pattern.source, `${pattern.flags}g`); |
75 | | - for (const match of text.matchAll(iter)) { |
76 | | - if (match.index === undefined) { |
77 | | - continue; |
78 | | - } |
79 | | - const end = match.index + match[0].length; |
80 | | - for (let i = match.index; i < end; i++) { |
81 | | - bitmap[i] = true; |
82 | | - } |
83 | | - } |
84 | | - } |
85 | | - return bitmap; |
86 | | -} |
87 | | - |
88 | 67 | // Iterate by full Unicode code point so astral-plane invisibles (e.g. U+E0061 TAG LATIN |
89 | 68 | // SMALL LETTER A, category Cf) are matched as single characters instead of being seen as a |
90 | 69 | // surrogate pair whose halves are category Cs and would escape the invisible-char regex. |
@@ -126,17 +105,16 @@ function sanitizeExecApprovalDisplayTextInternal( |
126 | 105 | if (strippedRedacted === stripped) { |
127 | 106 | return truncateForDisplay(escapeInvisibles(rawRedacted, options)); |
128 | 107 | } |
129 | | - // Detect bypass by position-bitmap coverage. Run each redaction pattern independently on |
130 | | - // both views and map stripped-view match positions back to original coordinates. If every |
131 | | - // position the stripped view would mask is also masked by the raw view, the raw view |
132 | | - // already covered everything — for example, an ordinary multi-line PEM private key where |
133 | | - // raw produces `BEGIN/…redacted…/END` while stripped collapses to `***`. A real bypass |
134 | | - // exists only when the stripped view masks at least one original position raw missed (e.g. |
135 | | - // the tail of an `sk-` token whose prefix-boundary was broken by a spliced zero-width or |
136 | | - // NBSP character). |
137 | | - const { patterns } = resolveRedactOptions({ mode: "tools" }); |
138 | | - const rawMask = computeRedactionBitmap(commandText, patterns); |
139 | | - const strippedMask = computeRedactionBitmap(stripped, patterns); |
| 108 | + // Detect bypass by position-bitmap coverage. Run the redaction matchers on both views and |
| 109 | + // map stripped-view match positions back to original coordinates. If every position the |
| 110 | + // stripped view would mask is also masked by the raw view, the raw view already covered |
| 111 | + // everything — for example, an ordinary multi-line PEM private key where raw produces |
| 112 | + // `BEGIN/…redacted…/END` while stripped collapses to `***`. A real bypass exists only when |
| 113 | + // the stripped view masks at least one original position raw missed (e.g. the tail of an |
| 114 | + // `sk-` token whose prefix-boundary was broken by a spliced zero-width or NBSP character). |
| 115 | + const redaction = resolveRedactOptions({ mode: "tools" }); |
| 116 | + const rawMask = computeSensitiveRedactionBitmap(commandText, redaction); |
| 117 | + const strippedMask = computeSensitiveRedactionBitmap(stripped, redaction); |
140 | 118 | let bypassDetected = false; |
141 | 119 | for (let i = 0; i < strippedMask.length; i++) { |
142 | 120 | if (strippedMask[i] && !rawMask[strippedToOrig[i]]) { |
|
0 commit comments