-
-
Notifications
You must be signed in to change notification settings - Fork 260
Expand file tree
/
Copy pathbundle-id-injection.test.ts
More file actions
97 lines (77 loc) · 3.51 KB
/
bundle-id-injection.test.ts
File metadata and controls
97 lines (77 loc) · 3.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
import { describe, it, expect } from 'vitest';
import { extractBundleIdFromAppPath } from '../bundle-id.ts';
import type { CommandExecutor } from '../CommandExecutor.ts';
/**
* CWE-78 regression tests for bundle-id.ts
*
* These tests verify that user-supplied appPath values containing shell
* metacharacters do NOT result in shell injection when passed through
* the executeSyncCommand → /bin/sh -c pipeline.
*
* CURRENT STATUS: These tests demonstrate the UNFIXED injection vectors
* identified in the review. The command string passed to /bin/sh -c
* contains unescaped user input, which would allow command injection.
*/
type CapturedCall = {
command: string[];
logPrefix?: string;
};
function createCapturingExecutor(calls: CapturedCall[]): CommandExecutor {
return async (command, logPrefix) => {
calls.push({ command: [...command], logPrefix });
// Simulate 'defaults' returning a fake bundle ID
return { success: true, output: 'com.example.app' };
};
}
describe('bundle-id.ts — CWE-78 shell injection vectors', () => {
it('UNFIXED: double-quote breakout in appPath reaches /bin/sh -c unescaped', async () => {
const calls: CapturedCall[] = [];
const executor = createCapturingExecutor(calls);
// Malicious appPath that breaks out of the double-quoted context
const maliciousPath = '/tmp/evil" $(id) "bar';
await extractBundleIdFromAppPath(maliciousPath, executor);
expect(calls).toHaveLength(1);
const shellCommand = calls[0].command;
// The command is ['/bin/sh', '-c', '...']
expect(shellCommand[0]).toBe('/bin/sh');
expect(shellCommand[1]).toBe('-c');
const cmdString = shellCommand[2];
// VULNERABILITY: The raw user input is interpolated directly into the
// shell command string. The $(id) is NOT escaped and would execute.
// A safe implementation would either:
// 1. Not use shell at all (pass args array to spawn directly), or
// 2. Properly escape the appPath with shellEscapeArg
//
// This test documents the current vulnerable behavior.
// When the fix is applied, update the assertion to verify safety.
expect(cmdString).toContain('$(id)');
// Verify the command reaches shell — it's using /bin/sh -c
expect(shellCommand[0]).toBe('/bin/sh');
});
it('UNFIXED: semicolon injection in appPath allows command chaining', async () => {
const calls: CapturedCall[] = [];
const executor = createCapturingExecutor(calls);
const maliciousPath = '/tmp/foo"; rm -rf / ; echo "';
await extractBundleIdFromAppPath(maliciousPath, executor);
const cmdString = calls[0].command[2];
// The rm -rf command is embedded in the shell string unescaped
expect(cmdString).toContain('rm -rf');
});
it('UNFIXED: backtick injection in appPath', async () => {
const calls: CapturedCall[] = [];
const executor = createCapturingExecutor(calls);
const maliciousPath = '/tmp/`touch /tmp/pwned`';
await extractBundleIdFromAppPath(maliciousPath, executor);
const cmdString = calls[0].command[2];
expect(cmdString).toContain('`touch /tmp/pwned`');
});
it('safe appPath without metacharacters works normally', async () => {
const calls: CapturedCall[] = [];
const executor = createCapturingExecutor(calls);
const safePath = '/Users/dev/Build/Products/Debug/MyApp.app';
const result = await extractBundleIdFromAppPath(safePath, executor);
expect(result).toBe('com.example.app');
expect(calls).toHaveLength(1);
expect(calls[0].command[2]).toContain(safePath);
});
});