fix(config): harden redaction against edge cases

Add comprehensive defensive checks to prevent RangeError:
- Filter null/undefined/non-string values
- Early return for empty sensitive values array
- Validate redaction sentinel
- Add test coverage for edge cases

Enhances fix for #41247, #40818
This commit is contained in:
Jarvis 2026-03-10 10:38:02 +08:00
parent 2d5e657931
commit 7e4e9d3371
2 changed files with 116 additions and 3 deletions

View File

@ -0,0 +1,87 @@
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 result = replaceSensitiveValuesInRaw({
raw: null as unknown as string,
sensitiveValues: ["test"],
redactedSentinel: "***",
});
// String(null) returns "null", but our defensive code returns empty string
expect(result).toBe("");
});
it("handles unicode strings", () => {
const result = replaceSensitiveValuesInRaw({
raw: '{"key": "🔑secret🔑"}',
sensitiveValues: ["🔑secret🔑"],
redactedSentinel: "***",
});
expect(result).toBe('{"key": "***"}');
});
});

View File

@ -1,18 +1,44 @@
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 (e.g., null, number), it is
* converted to a string via `String(params.raw ?? "")` and returned
* without redaction. This is a silent fallback for invalid input.
*/
export function replaceSensitiveValuesInRaw(params: {
raw: string;
sensitiveValues: string[];
redactedSentinel: string;
}): string {
// FIX #41247: Filter out empty strings to prevent RangeError
// Defensive: validate input types
if (typeof params.raw !== "string") {
return String(params.raw ?? "");
}
// Defensive: normalize and filter sensitive values
// Empty strings cause RangeError in String.replaceAll (#41247)
const values = [...params.sensitiveValues]
.filter((v) => v && v.length > 0)
.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;
}