Skip to content

Commit 0b8fa57

Browse files
committed
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.
1 parent 6ad1a6e commit 0b8fa57

4 files changed

Lines changed: 27 additions & 8 deletions

File tree

lib/arguments/options.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import {normalizeFdSpecificOptions} from './specific.js';
1717
// Normalize the options object, and sometimes also the file paths and arguments.
1818
// Applies default values, validate allowed options, normalize them.
1919
export const normalizeOptions = (filePath, rawArguments, rawOptions) => {
20-
rawOptions.cwd = normalizeCwd(rawOptions.cwd);
21-
const [processedFile, processedArguments, processedOptions] = handleNodeOption(filePath, rawArguments, rawOptions);
20+
// Prevent prototype pollution by copying only own properties to a null-prototype object
21+
const sanitizedOptions = {__proto__: null, ...rawOptions};
22+
sanitizedOptions.cwd = normalizeCwd(sanitizedOptions.cwd);
23+
const [processedFile, processedArguments, processedOptions] = handleNodeOption(filePath, rawArguments, sanitizedOptions);
2224

2325
const {command: file, args: commandArguments, options: initialOptions} = crossSpawn._parse(processedFile, processedArguments, processedOptions);
2426

@@ -43,6 +45,7 @@ export const normalizeOptions = (filePath, rawArguments, rawOptions) => {
4345
return {file, commandArguments, options};
4446
};
4547

48+
// Use null prototype to prevent prototype pollution from leaking through
4649
const addDefaultOptions = ({
4750
extendEnv = true,
4851
preferLocal = false,
@@ -61,6 +64,7 @@ const addDefaultOptions = ({
6164
serialization = 'advanced',
6265
...options
6366
}) => ({
67+
__proto__: null,
6468
...options,
6569
extendEnv,
6670
preferLocal,

lib/methods/bind.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import isPlainObject from 'is-plain-obj';
22
import {FD_SPECIFIC_OPTIONS} from '../arguments/specific.js';
33

44
// Deep merge specific options like `env`. Shallow merge the other ones.
5+
// Use spread (which only copies own properties) to safely read from boundOptions without prototype pollution
56
export const mergeOptions = (boundOptions, options) => {
6-
const newOptions = Object.fromEntries(
7+
const safeBoundOptions = {__proto__: null, ...boundOptions};
8+
const mergedOptions = Object.fromEntries(
79
Object.entries(options).map(([optionName, optionValue]) => [
810
optionName,
9-
mergeOption(optionName, boundOptions[optionName], optionValue),
11+
mergeOption(optionName, safeBoundOptions[optionName], optionValue),
1012
]),
1113
);
12-
return {...boundOptions, ...newOptions};
14+
return {...safeBoundOptions, ...mergedOptions};
1315
};
1416

1517
const mergeOption = (optionName, boundOptionValue, optionValue) => {

lib/methods/node.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ export const handleNodeOption = (file, commandArguments, {
2828

2929
const normalizedNodePath = safeNormalizeFileUrl(nodePath, 'The "nodePath" option');
3030
const resolvedNodePath = path.resolve(cwd, normalizedNodePath);
31+
// Use spread (which only copies own properties) to safely get shell without reading polluted prototype
3132
const newOptions = {
33+
__proto__: null,
34+
shell: false,
3235
...options,
3336
nodePath: resolvedNodePath,
3437
node: shouldHandleNode,
@@ -45,7 +48,16 @@ export const handleNodeOption = (file, commandArguments, {
4548

4649
return [
4750
resolvedNodePath,
48-
[...nodeOptions, file, ...commandArguments],
49-
{ipc: true, ...newOptions, shell: false},
51+
[
52+
...nodeOptions,
53+
file,
54+
...commandArguments
55+
],
56+
{
57+
__proto__: null,
58+
ipc: true,
59+
...newOptions,
60+
shell: false,
61+
},
5062
];
5163
};

lib/methods/parameters.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ export const normalizeParameters = (rawFile, rawArguments = [], rawOptions = {})
2727
throw new TypeError(`Last argument must be an options object: ${options}`);
2828
}
2929

30-
return [filePath, normalizedArguments, options];
30+
// Prevent prototype pollution by copying only own properties to a null-prototype object
31+
return [filePath, normalizedArguments, {__proto__: null, ...options}];
3132
};

0 commit comments

Comments
 (0)