Merge 4c740f7dd3dc00a0eb19df2595954dff94d8927b into 9fb78453e088cd7b553d7779faa0de5c83708e70
This commit is contained in:
commit
7622bae4a9
93
src/config/redact-snapshot.raw.test.ts
Normal file
93
src/config/redact-snapshot.raw.test.ts
Normal 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": "***"}');
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user