UNPKG

@hapi/bourne

Version:

JSON parse with prototype poisoning protection

73 lines (53 loc) 1.78 kB
const suspectRx = /"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/; export function parse(text, ...args) { // Normalize arguments const firstOptions = typeof args[0] === 'object' && args[0]; const reviver = args.length > 1 || !firstOptions ? args[0] : undefined; const options = (args.length > 1 && args[1]) || firstOptions || {}; // Parse normally, allowing exceptions const obj = JSON.parse(text, reviver); // options.protoAction: 'error' (default) / 'remove' / 'ignore' if (options.protoAction === 'ignore') { return obj; } // Ignore null and non-objects if (!obj || typeof obj !== 'object') { return obj; } // Check original string for potential exploit if (!suspectRx.test(text)) { return obj; } // Scan result for proto keys scan(obj, options); return obj; } export function scan(obj, options = {}) { let next = [obj]; while (next.length) { const nodes = next; next = []; for (const node of nodes) { if (Object.hasOwn(node, '__proto__')) { if (options.protoAction !== 'remove') { throw new SyntaxError('Object contains forbidden prototype property'); } delete node.__proto__; } for (const key in node) { const value = node[key]; if (value && typeof value === 'object') { next.push(node[key]); } } } } } export function safeParse(text, reviver) { try { return parse(text, reviver); } catch { return null; } }