Skip to content

Commit fb5caf4

Browse files
authored
Merge pull request #43 from snyk/dotkas/CLI-194/add-shescape-hardening
feat: [CLI-194] bumping shescape, adding hardening
2 parents f83f67a + 05b4289 commit fb5caf4

File tree

5 files changed

+40
-14
lines changed

5 files changed

+40
-14
lines changed

lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ async function cocoapodsVersion(root: string): Promise<string> {
181181
let podVersionOutput = '';
182182
try {
183183
// 1st: try to run CocoaPods via bundler
184-
podVersionOutput = await subProcess.execute('bundle exec pod', ['--version'], {cwd: root});
184+
podVersionOutput = await subProcess.execute('bundle', ['exec', 'pod', '--version'], {cwd: root});
185185
} catch {
186186
try {
187187
// 2nd: try to run CocoaPods directly

lib/sub-process.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as childProcess 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
export function execute(
56
command: string,
@@ -9,13 +10,26 @@ export function execute(
910
const spawnOptions: {
1011
shell: boolean;
1112
cwd?: string;
12-
} = { shell: true };
13+
} = { shell: false };
1314
if (options && options.cwd) {
1415
spawnOptions.cwd = options.cwd;
1516
}
1617

17-
args = quoteAll(args, { flagProtection: false });
18-
18+
if (args) {
19+
// Best practices, also security-wise, is to not invoke processes in a shell, but as a stand-alone command.
20+
// However, on Windows, we need to invoke the command in a shell, due to internal NodeJS problems with this approach
21+
// see: https://nodejs.org/docs/latest-v24.x/api/child_process.html#spawning-bat-and-cmd-files-on-windows
22+
const isWinLocal = /^win/.test(os.platform());
23+
if (isWinLocal) {
24+
spawnOptions.shell = true;
25+
// Further, we distinguish between quoting and escaping arguments since quoteAll does not support quoting without
26+
// supplying a shell, but escapeAll does.
27+
// See this (very long) discussion for more details: https://github.com/ericcornelissen/shescape/issues/2009
28+
args = quoteAll(args, { ...spawnOptions, flagProtection: false });
29+
} else {
30+
args = escapeAll(args, { ...spawnOptions, flagProtection: false });
31+
}
32+
}
1933
return new Promise((resolve, reject) => {
2034
let stdout = '';
2135
let stderr = '';
@@ -28,6 +42,12 @@ export function execute(
2842
stderr = stderr + data;
2943
});
3044

45+
// Handle spawn errors (e.g., ENOENT when command doesn't exist)
46+
proc.on('error', (error) => {
47+
stderr = error.message;
48+
reject({ stdout, stderr });
49+
});
50+
3151
proc.on('close', (code: number) => {
3252
if (code !== 0) {
3353
return reject(new Error(stdout || stderr));

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@snyk/cli-interface": "^2.11.0",
3535
"@snyk/cocoapods-lockfile-parser": "3.6.2",
3636
"@snyk/dep-graph": "^1.23.1",
37-
"shescape": "2.1.4",
37+
"shescape": "2.1.6",
3838
"source-map-support": "^0.5.7",
3939
"tslib": "^2.0.0"
4040
},

test/lib/index.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('inspect(rootDir, targetFile?, options?)', () => {
3636
});
3737

3838
expect(mockedExecute.mock.calls).toEqual([
39-
['bundle exec pod', ['--version'], { cwd: rootDir }],
39+
['bundle', ['exec', 'pod', '--version'], { cwd: rootDir }],
4040
]);
4141
});
4242

@@ -54,7 +54,7 @@ describe('inspect(rootDir, targetFile?, options?)', () => {
5454
});
5555

5656
expect(mockedExecute.mock.calls).toEqual([
57-
['bundle exec pod', ['--version'], { cwd: rootDir }],
57+
['bundle', ['exec', 'pod', '--version'], { cwd: rootDir }],
5858
['pod', ['--version'], { cwd: rootDir }],
5959
]);
6060
});
@@ -73,7 +73,7 @@ describe('inspect(rootDir, targetFile?, options?)', () => {
7373
});
7474

7575
expect(mockedExecute.mock.calls).toEqual([
76-
['bundle exec pod', ['--version'], { cwd: rootDir }],
76+
['bundle', ['exec', 'pod', '--version'], { cwd: rootDir }],
7777
['pod', ['--version'], { cwd: rootDir }],
7878
]);
7979
});

test/lib/sub-process.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,37 @@ import * as subProcess from '../../lib/sub-process';
33
describe('execute()', () => {
44
test('Resolves when the command succeeds', async () => {
55
await expect(
6-
subProcess.execute('echo "success" && echo "ignore" >&2 && true'),
6+
subProcess.execute('sh', [
7+
'-c',
8+
'echo "success" && echo "ignore" >&2 && true',
9+
]),
710
).resolves.toEqual('success\n');
811
});
912

1013
test('Resolves with stderr when the command succeeds and there is no stdout', async () => {
1114
await expect(
12-
subProcess.execute('echo "warn" >&2 && true'),
15+
subProcess.execute('sh', ['-c', 'echo "warn" >&2 && true']),
1316
).resolves.toMatch('warn\n');
1417
});
1518

1619
test('Rejects with stdout when the command fails', async () => {
1720
await expect(
18-
subProcess.execute('echo "error msg" && echo "ignore" >&2 && false'),
21+
subProcess.execute('sh', [
22+
'-c',
23+
'echo "error msg" && echo "ignore" >&2 && false',
24+
]),
1925
).rejects.toThrow('error msg\n');
2026
});
2127

2228
test('Rejects with stderr when the command fails and there is no stdout', async () => {
2329
await expect(
24-
subProcess.execute('echo "error msg" >&2 && false'),
30+
subProcess.execute('sh', ['-c', 'echo "error msg" >&2 && false']),
2531
).rejects.toThrow('error msg\n');
2632
});
2733

2834
test('Considers option.cwd', async () => {
2935
await expect(
30-
subProcess.execute('basename $PWD', [], { cwd: __dirname }),
36+
subProcess.execute('sh', ['-c', 'basename $PWD'], { cwd: __dirname }),
3137
).resolves.toEqual('lib\n');
3238
});
3339

0 commit comments

Comments
 (0)