Merge 4c740f7dd3dc00a0eb19df2595954dff94d8927b into 9fb78453e088cd7b553d7779faa0de5c83708e70

This commit is contained in:
Siew's Capital Jarvis 2026-03-21 13:11:43 +08:00 committed by GitHub
commit 7622bae4a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 123 additions and 2 deletions

View File

@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import { replaceSensitiveValuesInRaw } from "./redact-snapshot.raw.js";
describe("replaceSensitiveValuesInRaw", () => {
it("redacts sensitive values from raw string", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"apiKey": "secret123", "name": "test"}',
sensitiveValues: ["secret123"],
redactedSentinel: "__REDACTED__",
});
expect(result).toBe('{"apiKey": "__REDACTED__", "name": "test"}');
});
it("handles multiple sensitive values (longest first)", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"key1": "abc", "key2": "abcdef"}',
sensitiveValues: ["abc", "abcdef"],
redactedSentinel: "***",
});
expect(result).toBe('{"key1": "***", "key2": "***"}');
});
// Regression test for #41247
it("handles empty strings without throwing RangeError", () => {
expect(() =>
replaceSensitiveValuesInRaw({
raw: '{"key": "value"}',
sensitiveValues: ["", "value"],
redactedSentinel: "__REDACTED__",
}),
).not.toThrow();
const result = replaceSensitiveValuesInRaw({
raw: '{"key": "value"}',
sensitiveValues: ["", "value"],
redactedSentinel: "__REDACTED__",
});
expect(result).toBe('{"key": "__REDACTED__"}');
});
it("handles null and undefined values", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"key": "secret"}',
sensitiveValues: [null, undefined, "secret"] as unknown as string[],
redactedSentinel: "***",
});
expect(result).toBe('{"key": "***"}');
});
it("returns raw unchanged when no valid sensitive values", () => {
const raw = '{"key": "value"}';
const result = replaceSensitiveValuesInRaw({
raw,
sensitiveValues: ["", null, undefined] as unknown as string[],
redactedSentinel: "__REDACTED__",
});
expect(result).toBe(raw);
});
it("uses default sentinel when provided sentinel is empty", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"key": "secret"}',
sensitiveValues: ["secret"],
redactedSentinel: "",
});
expect(result).toBe('{"key": "__REDACTED__"}');
});
it("handles non-string raw input gracefully", () => {
const nullResult = replaceSensitiveValuesInRaw({
raw: null as unknown as string,
sensitiveValues: ["test"],
redactedSentinel: "***",
});
const objectResult = replaceSensitiveValuesInRaw({
raw: { secret: "test" } as unknown as string,
sensitiveValues: ["test"],
redactedSentinel: "***",
});
expect(nullResult).toBe("");
expect(objectResult).toBe("");
});
it("handles unicode strings", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"key": "🔑secret🔑"}',
sensitiveValues: ["🔑secret🔑"],
redactedSentinel: "***",
});
expect(result).toBe('{"key": "***"}');
});
});

View File

@ -1,15 +1,43 @@
import { isDeepStrictEqual } from "node:util";
import JSON5 from "json5";
/**
* Redacts sensitive values from a raw config string.
* Filters out empty/null/undefined values to prevent RangeError (#41247).
*
* Note: When `params.raw` is not a string, this returns an empty string
* defensively instead of returning a stringified unredacted value.
*/
export function replaceSensitiveValuesInRaw(params: {
raw: string;
sensitiveValues: string[];
redactedSentinel: string;
}): string {
const values = [...params.sensitiveValues].toSorted((a, b) => b.length - a.length);
// Defensive: validate input types
if (typeof params.raw !== "string") {
return "";
}
// Defensive: normalize and filter sensitive values
// Empty strings cause RangeError in String.replaceAll (#41247)
const values = [...params.sensitiveValues]
.filter((v): v is string => typeof v === "string" && v.length > 0)
.toSorted((a, b) => b.length - a.length);
// Early return if no valid values to redact
if (values.length === 0) {
return params.raw;
}
// Defensive: ensure sentinel is valid
const sentinel =
typeof params.redactedSentinel === "string" && params.redactedSentinel.length > 0
? params.redactedSentinel
: "__REDACTED__";
let result = params.raw;
for (const value of values) {
result = result.replaceAll(value, params.redactedSentinel);
result = result.replaceAll(value, sentinel);
}
return result;
}