fix(secrets): redact cut regex suffixes with context
This commit is contained in:
@@ -1197,15 +1197,15 @@ export class SecretObfuscator {
|
||||
// the secret's expanded value). Rewriting across the token drops bytes
|
||||
// (obfuscate) or drifts the redaction across re-obfuscation passes
|
||||
// (replace), so the cut secret must stay as its existing placeholder.
|
||||
// But the wholly-outside prefix BEFORE the placeholder is still
|
||||
// provider-visible content covered by the regex. Probe against the full
|
||||
// expanded scan text so right-hand context supplied by the placeholder
|
||||
// still satisfies lookahead/alternatives, then clamp the accepted match
|
||||
// to the prefix boundary so bytes owned by the placeholder stay atomic.
|
||||
// Otherwise skip past the placeholder and match trailing content fresh.
|
||||
// But wholly-outside bytes on either side of the placeholder are still
|
||||
// provider-visible content covered by the regex. Probe the prefix against
|
||||
// the full expanded scan text so right-hand context supplied by the
|
||||
// placeholder still satisfies lookahead/alternatives, then clamp the
|
||||
// accepted match to the prefix boundary. If no prefix is available, redact
|
||||
// the outside suffix that was covered by the full-context match.
|
||||
const cutResumeIndex = mapped.cutResumeIndex;
|
||||
const prefixScanEnd = mapped.firstPlaceholderScanStart;
|
||||
let handledPrefix = false;
|
||||
let handledOutside = false;
|
||||
if (prefixScanEnd > match.index) {
|
||||
regex.lastIndex = match.index;
|
||||
const prefixMatch = regex.exec(scanText);
|
||||
@@ -1220,11 +1220,25 @@ export class SecretObfuscator {
|
||||
scanMatchLength = scanMatchValue.length;
|
||||
mapped = prefixMapped;
|
||||
regex.lastIndex = prefixEnd;
|
||||
handledPrefix = true;
|
||||
handledOutside = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!handledPrefix) {
|
||||
if (!handledOutside && cutResumeIndex < end) {
|
||||
const suffixStart = cutResumeIndex;
|
||||
const suffixEnd = end;
|
||||
const suffixMapped = mapReplaceRegexMatch(regexScan.segments, suffixStart, suffixEnd);
|
||||
if (!suffixMapped.partialPlaceholderCut) {
|
||||
start = suffixStart;
|
||||
end = suffixEnd;
|
||||
scanMatchValue = scanText.slice(suffixStart, suffixEnd);
|
||||
scanMatchLength = scanMatchValue.length;
|
||||
mapped = suffixMapped;
|
||||
regex.lastIndex = suffixEnd;
|
||||
handledOutside = true;
|
||||
}
|
||||
}
|
||||
if (!handledOutside) {
|
||||
regex.lastIndex = cutResumeIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -663,6 +663,26 @@ describe("SecretObfuscator friendlyName placeholders", () => {
|
||||
expect(obf.obfuscate(second)).toBe(second);
|
||||
});
|
||||
|
||||
it("redacts a cut suffix using placeholder left context instead of leaking it", () => {
|
||||
const key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1n";
|
||||
const entries = [
|
||||
{ type: "plain" as const, content: "ABCDEFGH" },
|
||||
{ type: "regex" as const, mode: "replace" as const, content: "(?<=ABCD)[A-Z]{8}" },
|
||||
];
|
||||
const obf = new SecretObfuscator(entries, key);
|
||||
const placeholdered = obf.obfuscate("ABCDEFGH");
|
||||
const second = obf.obfuscate("ABCDEFGHIJKL");
|
||||
|
||||
// The suffix match depends on lookbehind supplied by the expanded placeholder.
|
||||
// It must still be redacted while the placeholder's own bytes stay atomic.
|
||||
expect(second).not.toContain("IJKL");
|
||||
expect(second).toContain(placeholdered);
|
||||
expect(obf.obfuscate(second)).toBe(second);
|
||||
|
||||
const restarted = new SecretObfuscator(entries, key);
|
||||
expect(restarted.obfuscate(second)).toBe(second);
|
||||
});
|
||||
|
||||
it("keeps default replace markers stable when a lookbehind match spills into a prior placeholder", () => {
|
||||
const key = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1n";
|
||||
const entries = [
|
||||
|
||||
Reference in New Issue
Block a user