Skip to content

Commit c31919e

Browse files
committed
feat: hardening subprocess logic
1 parent 1dbb911 commit c31919e

File tree

2 files changed

+41
-9
lines changed

2 files changed

+41
-9
lines changed

lib/dependencies/sub-process.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,41 @@
11
import { spawn, SpawnOptions, spawnSync } from 'child_process';
2-
import { quoteAll } from 'shescape/stateless';
2+
import { escapeAll, quoteAll } from 'shescape/stateless';
3+
import * as os from 'node:os';
34

45
interface ProcessOptions {
56
cwd?: string;
67
env?: { [name: string]: string };
78
}
89

9-
function makeSpawnOptions(options?: ProcessOptions) {
10+
function processArguments(
11+
args: string[],
12+
spawnOptions: SpawnOptions
13+
): string[] {
14+
if (!args) {
15+
return args;
16+
}
17+
18+
// Best practices, also security-wise, is to not invoke processes in a shell, but as a stand-alone command.
19+
// However, on Windows, we need to invoke the command in a shell, due to internal NodeJS problems with this approach
20+
// see: https://nodejs.org/docs/latest-v24.x/api/child_process.html#spawning-bat-and-cmd-files-on-windows
21+
const isWinLocal = /^win/.test(os.platform());
22+
if (isWinLocal) {
23+
spawnOptions.shell = true;
24+
// Further, we distinguish between quoting and escaping arguments since quoteAll does not support quoting without
25+
// supplying a shell, but escapeAll does.
26+
// See this (very long) discussion for more details: https://github.com/ericcornelissen/shescape/issues/2009
27+
return quoteAll(args, { ...spawnOptions, flagProtection: false });
28+
} else {
29+
return escapeAll(args, { ...spawnOptions, flagProtection: false });
30+
}
31+
}
32+
33+
function makeSpawnOptions(options?: ProcessOptions): SpawnOptions {
1034
const spawnOptions: SpawnOptions = {
11-
shell: true,
35+
shell: false,
1236
env: { ...process.env },
1337
};
38+
1439
if (options && options.cwd) {
1540
spawnOptions.cwd = options.cwd;
1641
}
@@ -19,7 +44,7 @@ function makeSpawnOptions(options?: ProcessOptions) {
1944
}
2045

2146
// Before spawning an external process, we look if we need to restore the system proxy configuration,
22-
// which overides the cli internal proxy configuration.
47+
// which overrides the cli internal proxy configuration.
2348
if (process.env.SNYK_SYSTEM_HTTP_PROXY !== undefined) {
2449
spawnOptions.env.HTTP_PROXY = process.env.SNYK_SYSTEM_HTTP_PROXY;
2550
}
@@ -39,19 +64,26 @@ export function execute(
3964
options?: ProcessOptions
4065
): Promise<string> {
4166
const spawnOptions = makeSpawnOptions(options);
42-
args = quoteAll(args, { flagProtection: false });
67+
const processedArgs = processArguments(args, spawnOptions);
68+
4369
return new Promise((resolve, reject) => {
4470
let stdout = '';
4571
let stderr = '';
4672

47-
const proc = spawn(command, args, spawnOptions);
73+
const proc = spawn(command, processedArgs, spawnOptions);
4874
proc.stdout.on('data', (data) => {
4975
stdout = stdout + data;
5076
});
77+
5178
proc.stderr.on('data', (data) => {
5279
stderr = stderr + data;
5380
});
5481

82+
proc.on('error', (err) => {
83+
stderr = err.message;
84+
return reject({ stdout, stderr });
85+
});
86+
5587
proc.on('close', (code) => {
5688
if (code !== 0) {
5789
return reject(stdout || stderr);
@@ -67,7 +99,7 @@ export function executeSync(
6799
options?: ProcessOptions
68100
) {
69101
const spawnOptions = makeSpawnOptions(options);
70-
args = quoteAll(args, { flagProtection: false });
102+
const processedArgs = processArguments(args, spawnOptions);
71103

72-
return spawnSync(command, args, spawnOptions);
104+
return spawnSync(command, processedArgs, spawnOptions);
73105
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"dependencies": {
2727
"@snyk/cli-interface": "^2.11.2",
2828
"@snyk/dep-graph": "^1.28.1",
29-
"shescape": "2.1.4",
29+
"shescape": "2.1.6",
3030
"snyk-poetry-lockfile-parser": "^1.9.0",
3131
"tmp": "0.2.3"
3232
},

0 commit comments

Comments
 (0)