From 70a482ed7a61ebd63834bb8271dd222ac4934246 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 21 Jan 2026 08:26:58 +0700 Subject: [PATCH] Harden against prototype pollution This is not fixing any vulnerability since prototype pollution is a app-level concern, but we can be nice and harden it in case the app makes such a mistake. --- lib/arguments/options.js | 8 ++++++-- lib/methods/bind.js | 8 +++++--- lib/methods/node.js | 16 ++++++++++++++-- lib/methods/parameters.js | 3 ++- 4 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/arguments/options.js b/lib/arguments/options.js index 5f591026a1..7919e5fcd3 100644 --- a/lib/arguments/options.js +++ b/lib/arguments/options.js @@ -17,8 +17,10 @@ import {normalizeFdSpecificOptions} from './specific.js'; // Normalize the options object, and sometimes also the file paths and arguments. // Applies default values, validate allowed options, normalize them. export const normalizeOptions = (filePath, rawArguments, rawOptions) => { - rawOptions.cwd = normalizeCwd(rawOptions.cwd); - const [processedFile, processedArguments, processedOptions] = handleNodeOption(filePath, rawArguments, rawOptions); + // Prevent prototype pollution by copying only own properties to a null-prototype object + const sanitizedOptions = {__proto__: null, ...rawOptions}; + sanitizedOptions.cwd = normalizeCwd(sanitizedOptions.cwd); + const [processedFile, processedArguments, processedOptions] = handleNodeOption(filePath, rawArguments, sanitizedOptions); const {command: file, args: commandArguments, options: initialOptions} = crossSpawn._parse(processedFile, processedArguments, processedOptions); @@ -43,6 +45,7 @@ export const normalizeOptions = (filePath, rawArguments, rawOptions) => { return {file, commandArguments, options}; }; +// Use null prototype to prevent prototype pollution from leaking through const addDefaultOptions = ({ extendEnv = true, preferLocal = false, @@ -61,6 +64,7 @@ const addDefaultOptions = ({ serialization = 'advanced', ...options }) => ({ + __proto__: null, ...options, extendEnv, preferLocal, diff --git a/lib/methods/bind.js b/lib/methods/bind.js index d5fae18c20..dd1b285eb2 100644 --- a/lib/methods/bind.js +++ b/lib/methods/bind.js @@ -2,14 +2,16 @@ import isPlainObject from 'is-plain-obj'; import {FD_SPECIFIC_OPTIONS} from '../arguments/specific.js'; // Deep merge specific options like `env`. Shallow merge the other ones. +// Use spread (which only copies own properties) to safely read from boundOptions without prototype pollution export const mergeOptions = (boundOptions, options) => { - const newOptions = Object.fromEntries( + const safeBoundOptions = {__proto__: null, ...boundOptions}; + const mergedOptions = Object.fromEntries( Object.entries(options).map(([optionName, optionValue]) => [ optionName, - mergeOption(optionName, boundOptions[optionName], optionValue), + mergeOption(optionName, safeBoundOptions[optionName], optionValue), ]), ); - return {...boundOptions, ...newOptions}; + return {...safeBoundOptions, ...mergedOptions}; }; const mergeOption = (optionName, boundOptionValue, optionValue) => { diff --git a/lib/methods/node.js b/lib/methods/node.js index 80d25d6d5f..8fdbc36d11 100644 --- a/lib/methods/node.js +++ b/lib/methods/node.js @@ -28,7 +28,10 @@ export const handleNodeOption = (file, commandArguments, { const normalizedNodePath = safeNormalizeFileUrl(nodePath, 'The "nodePath" option'); const resolvedNodePath = path.resolve(cwd, normalizedNodePath); + // Use spread (which only copies own properties) to safely get shell without reading polluted prototype const newOptions = { + __proto__: null, + shell: false, ...options, nodePath: resolvedNodePath, node: shouldHandleNode, @@ -45,7 +48,16 @@ export const handleNodeOption = (file, commandArguments, { return [ resolvedNodePath, - [...nodeOptions, file, ...commandArguments], - {ipc: true, ...newOptions, shell: false}, + [ + ...nodeOptions, + file, + ...commandArguments, + ], + { + __proto__: null, + ipc: true, + ...newOptions, + shell: false, + }, ]; }; diff --git a/lib/methods/parameters.js b/lib/methods/parameters.js index c4e526fa1c..76f8fa8206 100644 --- a/lib/methods/parameters.js +++ b/lib/methods/parameters.js @@ -27,5 +27,6 @@ export const normalizeParameters = (rawFile, rawArguments = [], rawOptions = {}) throw new TypeError(`Last argument must be an options object: ${options}`); } - return [filePath, normalizedArguments, options]; + // Prevent prototype pollution by copying only own properties to a null-prototype object + return [filePath, normalizedArguments, {__proto__: null, ...options}]; };