Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 37 additions & 19 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,30 @@ const suspectProtoRx =
const suspectConstructorRx =
/"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;

const JsonSigRx = /^\s*["[{]|^\s*-?\d{1,16}(\.\d{1,17})?([Ee][+-]?\d+)?\s*$/;

function jsonParseTransform(key: string, value: any): any {
if (
key === "__proto__" ||
(key === "constructor" &&
value &&
typeof value === "object" &&
"prototype" in value)
) {
warnKeyDropped(key);
return;
}
return value;
}
const JsonSigRx = /^\s*["[{]|^\s*-?\d+(\.\d+)?([Ee][+-]?\d+)?\s*$/;

function warnKeyDropped(key: string): void {
console.warn(`[destr] Dropping "${key}" key to prevent prototype pollution.`);
}

export type Options = {
strict?: boolean;
reviver?: (this: any, key: string, value: any) => any;
};

export function destr<T = unknown>(value: any, options: Options = {}): T {
export function destr<T = unknown>(
value: any,
optionsOrReviver?: Options | ((this: any, key: string, value: any) => any),
): T {
if (typeof value !== "string") {
return value;
}

const options =
typeof optionsOrReviver === "function"
? { reviver: optionsOrReviver }
: optionsOrReviver || {};

if (
value[0] === '"' &&
value[value.length - 1] === '"' &&
Expand Down Expand Up @@ -81,9 +77,24 @@ export function destr<T = unknown>(value: any, options: Options = {}): T {
if (options.strict) {
throw new Error("[destr] Possible prototype pollution");
}
return JSON.parse(value, jsonParseTransform);
return JSON.parse(value, (key, val) => {
if (
key === "__proto__" ||
(key === "constructor" &&
val &&
typeof val === "object" &&
"prototype" in val)
) {
warnKeyDropped(key);
return;
}
if (options.reviver) {
return options.reviver(key, val);
}
return val;
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
return JSON.parse(value);
return JSON.parse(value, options.reviver);
} catch (error) {
if (options.strict) {
throw error;
Expand All @@ -92,7 +103,14 @@ export function destr<T = unknown>(value: any, options: Options = {}): T {
}
}

export function safeDestr<T = unknown>(value: any, options: Options = {}): T {
export function safeDestr<T = unknown>(
value: any,
optionsOrReviver?: Options | ((this: any, key: string, value: any) => any),
): T {
const options =
typeof optionsOrReviver === "function"
? { reviver: optionsOrReviver }
: optionsOrReviver || {};
return destr<T>(value, { ...options, strict: true });
}

Expand Down
41 changes: 41 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,45 @@ describe("destr", () => {
expect(destr(testCase.input)).toStrictEqual(testCase.output);
}
});

it("parses huge numbers without throwing in safeDestr", () => {
const hugeNumber = "123456789012345678901234567890";
// 默认模式下应该解析为数字类型(即便有精度损失,也应符合原生 JSON.parse 行为)
expect(typeof destr(hugeNumber)).toBe("number");
// 严格模式下不应抛出 "Invalid JSON" 错误
expect(typeof safeDestr(hugeNumber)).toBe("number");
});

describe("reviver support", () => {
it("supports reviver as an option", () => {
const input = '{"a": 1, "b": 2}';
const reviver = (key: string, value: any) => {
if (key === "a") return value * 10;
return value;
};
expect(destr(input, { reviver })).toStrictEqual({ a: 10, b: 2 });
expect(safeDestr(input, { reviver })).toStrictEqual({ a: 10, b: 2 });
});

it("supports reviver as the second argument directly", () => {
const input = '{"a": 1, "b": 2}';
const reviver = (key: string, value: any) => {
if (key === "a") return value * 10;
return value;
};
expect(destr(input, reviver)).toStrictEqual({ a: 10, b: 2 });
});

it("chains reviver with prototype pollution filter", () => {
const input = '{"a": 1, "__proto__": {"polluted": true}}';
const reviver = vi.fn((key: string, value: any) => value);

const result = destr(input, { reviver });

expect(result).toStrictEqual({ a: 1 });
// __proto__ 键应该在到达用户指定的 reviver 前被过滤,因此不应触发 reviver
expect(reviver).not.toHaveBeenCalledWith("__proto__", expect.any(Object));
expect(reviver).toHaveBeenCalledWith("a", 1);
});
});
});