@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# Command-Injection Remediation Playbook
|
|
2
|
+
|
|
3
|
+
The universal pattern: switch to the argument-vector form of your
|
|
4
|
+
language's process-spawn API. Specific snippets per language below.
|
|
5
|
+
|
|
6
|
+
## Python — subprocess (list form, shell=False)
|
|
7
|
+
|
|
8
|
+
### Before
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
import subprocess
|
|
12
|
+
subprocess.run(f"convert {filename} out.png", shell=True)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### After
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import subprocess
|
|
19
|
+
subprocess.run(["convert", filename, "out.png"], check=True)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Piping between commands without shell
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
# Before (shell pipeline)
|
|
26
|
+
subprocess.run(f"cat {filename} | grep error", shell=True)
|
|
27
|
+
|
|
28
|
+
# After (Popen chain)
|
|
29
|
+
with subprocess.Popen(["cat", filename], stdout=subprocess.PIPE) as p1:
|
|
30
|
+
subprocess.run(["grep", "error"], stdin=p1.stdout, check=True)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Capturing output
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
["convert", filename, "out.png"],
|
|
38
|
+
capture_output=True, text=True, check=True,
|
|
39
|
+
)
|
|
40
|
+
print(result.stdout)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Node.js — child_process spawn / execFile
|
|
44
|
+
|
|
45
|
+
### Before
|
|
46
|
+
|
|
47
|
+
```javascript
|
|
48
|
+
const { exec } = require('child_process');
|
|
49
|
+
exec(`convert ${filename} out.png`, (err, stdout) => {});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### After (execFile)
|
|
53
|
+
|
|
54
|
+
```javascript
|
|
55
|
+
const { execFile } = require('child_process');
|
|
56
|
+
execFile('convert', [filename, 'out.png'], (err, stdout) => {});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### After (spawn for streaming)
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
const { spawn } = require('child_process');
|
|
63
|
+
const child = spawn('convert', [filename, 'out.png']);
|
|
64
|
+
child.stdout.on('data', chunk => process.stdout.write(chunk));
|
|
65
|
+
child.on('close', code => console.log(`exit ${code}`));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### TypeScript with promisify
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { execFile as execFileCallback } from 'child_process';
|
|
72
|
+
import { promisify } from 'util';
|
|
73
|
+
const execFile = promisify(execFileCallback);
|
|
74
|
+
|
|
75
|
+
const { stdout } = await execFile('convert', [filename, 'out.png']);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Ruby — Open3.capture3 / Process.spawn
|
|
79
|
+
|
|
80
|
+
### Before
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
output = `convert #{filename} out.png`
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### After (Open3 — best for capturing output)
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
require 'open3'
|
|
90
|
+
stdout, stderr, status = Open3.capture3('convert', filename, 'out.png')
|
|
91
|
+
raise "convert failed: #{stderr}" unless status.success?
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### After (Process.spawn for fire-and-forget)
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
pid = Process.spawn('convert', filename, 'out.png')
|
|
98
|
+
Process.wait(pid)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### After (system with explicit args)
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
# system with multiple args bypasses shell
|
|
105
|
+
system('convert', filename, 'out.png') or raise "convert failed"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Go — exec.Command with argv
|
|
109
|
+
|
|
110
|
+
### Before
|
|
111
|
+
|
|
112
|
+
```go
|
|
113
|
+
cmd := exec.Command("sh", "-c", fmt.Sprintf("convert %s out.png", filename))
|
|
114
|
+
output, _ := cmd.CombinedOutput()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### After
|
|
118
|
+
|
|
119
|
+
```go
|
|
120
|
+
cmd := exec.Command("convert", filename, "out.png")
|
|
121
|
+
output, err := cmd.CombinedOutput()
|
|
122
|
+
if err != nil {
|
|
123
|
+
return fmt.Errorf("convert failed: %w", err)
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### With context (cancellation / timeout)
|
|
128
|
+
|
|
129
|
+
```go
|
|
130
|
+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
131
|
+
defer cancel()
|
|
132
|
+
cmd := exec.CommandContext(ctx, "convert", filename, "out.png")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Java — ProcessBuilder with array
|
|
136
|
+
|
|
137
|
+
### Before
|
|
138
|
+
|
|
139
|
+
```java
|
|
140
|
+
Runtime.getRuntime().exec("convert " + filename + " out.png");
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### After
|
|
144
|
+
|
|
145
|
+
```java
|
|
146
|
+
ProcessBuilder pb = new ProcessBuilder("convert", filename, "out.png");
|
|
147
|
+
pb.redirectErrorStream(true);
|
|
148
|
+
Process p = pb.start();
|
|
149
|
+
int exitCode = p.waitFor();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### With output capture
|
|
153
|
+
|
|
154
|
+
```java
|
|
155
|
+
try (BufferedReader reader = new BufferedReader(
|
|
156
|
+
new InputStreamReader(p.getInputStream()))) {
|
|
157
|
+
String line;
|
|
158
|
+
while ((line = reader.readLine()) != null) {
|
|
159
|
+
System.out.println(line);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## PHP — proc_open with array form
|
|
165
|
+
|
|
166
|
+
PHP 7.4+ supports passing an array as the command to `proc_open`,
|
|
167
|
+
which bypasses shell entirely:
|
|
168
|
+
|
|
169
|
+
### Before
|
|
170
|
+
|
|
171
|
+
```php
|
|
172
|
+
$output = shell_exec("convert $filename out.png");
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### After (PHP 7.4+)
|
|
176
|
+
|
|
177
|
+
```php
|
|
178
|
+
$descriptors = [
|
|
179
|
+
0 => ['pipe', 'r'],
|
|
180
|
+
1 => ['pipe', 'w'],
|
|
181
|
+
2 => ['pipe', 'w'],
|
|
182
|
+
];
|
|
183
|
+
$process = proc_open(
|
|
184
|
+
['convert', $filename, 'out.png'],
|
|
185
|
+
$descriptors,
|
|
186
|
+
$pipes
|
|
187
|
+
);
|
|
188
|
+
$stdout = stream_get_contents($pipes[1]);
|
|
189
|
+
fclose($pipes[1]);
|
|
190
|
+
proc_close($process);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Legacy PHP fallback (with escapeshellarg)
|
|
194
|
+
|
|
195
|
+
```php
|
|
196
|
+
$cmd = sprintf(
|
|
197
|
+
"convert %s %s",
|
|
198
|
+
escapeshellarg($filename),
|
|
199
|
+
escapeshellarg($outputName)
|
|
200
|
+
);
|
|
201
|
+
$output = shell_exec($cmd);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
`escapeshellarg()` wraps the argument in single quotes and escapes
|
|
205
|
+
any embedded single quotes. Defense-in-depth; the no-shell form
|
|
206
|
+
is still stricter.
|
|
207
|
+
|
|
208
|
+
## C# / .NET — Process.Start with ArgumentList
|
|
209
|
+
|
|
210
|
+
### Before
|
|
211
|
+
|
|
212
|
+
```csharp
|
|
213
|
+
Process.Start("cmd.exe", "/c convert " + filename + " out.png");
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### After (.NET Core 2.1+)
|
|
217
|
+
|
|
218
|
+
```csharp
|
|
219
|
+
var psi = new ProcessStartInfo("convert")
|
|
220
|
+
{
|
|
221
|
+
UseShellExecute = false,
|
|
222
|
+
RedirectStandardOutput = true,
|
|
223
|
+
};
|
|
224
|
+
psi.ArgumentList.Add(filename);
|
|
225
|
+
psi.ArgumentList.Add("out.png");
|
|
226
|
+
var process = Process.Start(psi);
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
`ArgumentList` (not `Arguments` string) is the safe path.
|
|
230
|
+
|
|
231
|
+
## Rust — std::process::Command
|
|
232
|
+
|
|
233
|
+
```rust
|
|
234
|
+
use std::process::Command;
|
|
235
|
+
|
|
236
|
+
// Safe by design — Command takes args separately
|
|
237
|
+
let output = Command::new("convert")
|
|
238
|
+
.arg(&filename)
|
|
239
|
+
.arg("out.png")
|
|
240
|
+
.output()
|
|
241
|
+
.expect("convert failed");
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
`std::process::Command` doesn't have a shell-wrapped form. Even if
|
|
245
|
+
you wanted one, you'd have to explicitly invoke `sh -c` yourself.
|
|
246
|
+
|
|
247
|
+
## Pre-commit integration
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
# .pre-commit-config.yaml
|
|
251
|
+
repos:
|
|
252
|
+
- repo: local
|
|
253
|
+
hooks:
|
|
254
|
+
- id: scan-cmdi
|
|
255
|
+
name: Scan for command-injection patterns
|
|
256
|
+
entry: python3 plugins/security/penetration-tester/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py
|
|
257
|
+
language: system
|
|
258
|
+
args: ['--min-severity', 'high']
|
|
259
|
+
pass_filenames: false
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## CI integration
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
- name: Command-injection scan
|
|
266
|
+
run: |
|
|
267
|
+
python3 plugins/security/penetration-tester/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py \
|
|
268
|
+
. --min-severity high --format json --output cmdi-scan.json
|
|
269
|
+
- run: |
|
|
270
|
+
if jq 'length > 0' cmdi-scan.json | grep -q true; then
|
|
271
|
+
echo "::error::Command-injection pattern detected"
|
|
272
|
+
cat cmdi-scan.json
|
|
273
|
+
exit 1
|
|
274
|
+
fi
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
## Defense-in-depth additions
|
|
278
|
+
|
|
279
|
+
For code that legitimately shells out to a binary:
|
|
280
|
+
|
|
281
|
+
1. **Run the binary as a low-privilege user.** Container-isolated,
|
|
282
|
+
no write access to host paths.
|
|
283
|
+
2. **Validate inputs against an allow-list before passing.**
|
|
284
|
+
Filenames matching `^[\w.-]+\.(png|jpg|webp)$`; refuse anything
|
|
285
|
+
else.
|
|
286
|
+
3. **Drop unused capabilities.** Linux: capset to drop network,
|
|
287
|
+
raw-socket, etc. for the spawned process.
|
|
288
|
+
4. **Use a dedicated processing queue.** Untrusted file processing
|
|
289
|
+
should run in a sandboxed worker, not the application server's
|
|
290
|
+
main process.
|
|
291
|
+
|
|
292
|
+
## Verification after remediation
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py \
|
|
296
|
+
/path/to/repo --min-severity medium
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Expected: exit 0, zero MEDIUM-or-higher findings. Remaining LOW
|
|
300
|
+
findings on `shell=True` with verified static-string arguments are
|
|
301
|
+
acceptable but should be migrated when the surrounding code is
|
|
302
|
+
touched anyway.
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# Command-Injection Theory
|
|
2
|
+
|
|
3
|
+
## The recurring shape
|
|
4
|
+
|
|
5
|
+
Application calls out to a binary for some task: image processing,
|
|
6
|
+
archive extraction, video conversion, DNS resolution, network ping,
|
|
7
|
+
"call this one CLI tool." The naive implementation builds a string,
|
|
8
|
+
passes it to a shell-invocation API. The shell parses the string
|
|
9
|
+
with full shell semantics: `;`, `|`, `&`, `$()`, backticks,
|
|
10
|
+
redirection.
|
|
11
|
+
|
|
12
|
+
The fix is universal: don't go through the shell. Most APIs have
|
|
13
|
+
a list-of-arguments form that bypasses the shell entirely, treating
|
|
14
|
+
each list element as a literal argument to the binary's `execve()`.
|
|
15
|
+
|
|
16
|
+
## Why `shell=True` is the default footgun
|
|
17
|
+
|
|
18
|
+
Python's `subprocess.run(["ls", "-la"])` is safe — `argv` is passed
|
|
19
|
+
to `execve()` directly. `subprocess.run("ls -la", shell=True)` is
|
|
20
|
+
NOT safe because the string goes through `/bin/sh -c`.
|
|
21
|
+
|
|
22
|
+
The footgun: the list form looks more verbose. New engineers
|
|
23
|
+
default to the string form because it reads like the shell
|
|
24
|
+
command they're used to typing. `shell=True` becomes a
|
|
25
|
+
convenience reflex, then becomes a habit, then ships.
|
|
26
|
+
|
|
27
|
+
Same pattern in every language:
|
|
28
|
+
|
|
29
|
+
- Node `exec("cmd arg")` (shell) vs `spawn("cmd", ["arg"])` (no shell)
|
|
30
|
+
- Ruby `` `cmd #{var}` `` (shell) vs `Open3.capture3("cmd", var)` (no shell)
|
|
31
|
+
- Go `exec.Command("sh", "-c", cmd)` (shell) vs `exec.Command("cmd", arg)` (no shell)
|
|
32
|
+
- Java `Runtime.exec(String)` (shell-tokenized) vs `Runtime.exec(String[])` (no shell)
|
|
33
|
+
- PHP `system($cmd)` (shell) vs `pcntl_exec($cmd, [$arg])` (no shell)
|
|
34
|
+
|
|
35
|
+
## The argument-vector vs command-string distinction
|
|
36
|
+
|
|
37
|
+
When the OS executes a process via `execve()`, it takes an
|
|
38
|
+
**argument vector** — an array of strings. The first element is the
|
|
39
|
+
program path; subsequent elements are arguments.
|
|
40
|
+
|
|
41
|
+
A shell-interpreted command string gets tokenized into argv by the
|
|
42
|
+
shell, applying shell rules: word splitting on whitespace, glob
|
|
43
|
+
expansion, variable substitution, command substitution, quoting,
|
|
44
|
+
escape sequences, control operators.
|
|
45
|
+
|
|
46
|
+
The injection vector is exactly this tokenization step. If you
|
|
47
|
+
build a string `convert input.jpg output.png` and the attacker
|
|
48
|
+
controls `input.jpg`, they substitute `input.jpg; rm -rf /`. The
|
|
49
|
+
shell sees two commands separated by `;`.
|
|
50
|
+
|
|
51
|
+
If you build an argv `["convert", input_filename, output_filename]`
|
|
52
|
+
and pass it to `execve()` directly, the attacker substituting
|
|
53
|
+
`input.jpg; rm -rf /` as `input_filename` results in a single
|
|
54
|
+
argv element with a literal semicolon in it. `convert` receives
|
|
55
|
+
that as a filename argument, sees no such file, returns an error.
|
|
56
|
+
No second command runs.
|
|
57
|
+
|
|
58
|
+
## When you DO need a shell (rare)
|
|
59
|
+
|
|
60
|
+
Some legitimate use cases require shell semantics:
|
|
61
|
+
|
|
62
|
+
1. **Pipe between two commands.** `cmd1 | cmd2` requires a shell
|
|
63
|
+
to set up the pipe. Use the list-form Popen with `stdin=` /
|
|
64
|
+
`stdout=` to chain processes without a shell.
|
|
65
|
+
|
|
66
|
+
2. **Shell glob expansion.** `cmd /tmp/*.log` requires shell to
|
|
67
|
+
expand the glob. Better: use Python's `glob.glob()` /
|
|
68
|
+
Node's `glob` package / etc. to expand to a list, then pass
|
|
69
|
+
that list as argv.
|
|
70
|
+
|
|
71
|
+
3. **Environment variable expansion in the command string.**
|
|
72
|
+
`cmd $HOME/foo` requires shell. Better: read the env var in
|
|
73
|
+
your application code, pass the expanded string as an argv
|
|
74
|
+
element.
|
|
75
|
+
|
|
76
|
+
In every "I need shell semantics" case, the right fix is to move
|
|
77
|
+
the shell-semantic operation into your application code, then
|
|
78
|
+
call out to the binary with explicit argv.
|
|
79
|
+
|
|
80
|
+
## Per-language idioms
|
|
81
|
+
|
|
82
|
+
### Python — `subprocess` is the right module
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import subprocess
|
|
86
|
+
|
|
87
|
+
# UNSAFE — shell builds the string
|
|
88
|
+
subprocess.run(f"convert {filename} out.png", shell=True)
|
|
89
|
+
|
|
90
|
+
# SAFE — argv list, no shell
|
|
91
|
+
subprocess.run(["convert", filename, "out.png"])
|
|
92
|
+
|
|
93
|
+
# SAFE — input/output redirection via subprocess args, not shell
|
|
94
|
+
with open("out.txt", "w") as f:
|
|
95
|
+
subprocess.run(["my-command"], stdout=f)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`subprocess.run()` and `subprocess.Popen()` both accept a list
|
|
99
|
+
argument and default to `shell=False`. Use them.
|
|
100
|
+
|
|
101
|
+
### Node.js — `child_process.spawn` over `exec`
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const { spawn, execFile } = require('child_process');
|
|
105
|
+
|
|
106
|
+
// UNSAFE — shell builds the string
|
|
107
|
+
const child = exec(`convert ${filename} out.png`);
|
|
108
|
+
|
|
109
|
+
// SAFE — argv list, no shell
|
|
110
|
+
const child = spawn('convert', [filename, 'out.png']);
|
|
111
|
+
|
|
112
|
+
// SAFE — execFile is also no-shell by default
|
|
113
|
+
execFile('convert', [filename, 'out.png'], (err, stdout) => { ... });
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Ruby — `Open3.capture3` over backticks
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
require 'open3'
|
|
120
|
+
|
|
121
|
+
# UNSAFE — backticks invoke shell
|
|
122
|
+
output = `convert #{filename} out.png`
|
|
123
|
+
|
|
124
|
+
# SAFE — argv list
|
|
125
|
+
stdout, stderr, status = Open3.capture3('convert', filename, 'out.png')
|
|
126
|
+
|
|
127
|
+
# SAFE — Process.spawn list form
|
|
128
|
+
pid = Process.spawn('convert', filename, 'out.png')
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Go — `exec.Command` with explicit args
|
|
132
|
+
|
|
133
|
+
```go
|
|
134
|
+
import "os/exec"
|
|
135
|
+
|
|
136
|
+
// UNSAFE — shell wrapper
|
|
137
|
+
cmd := exec.Command("sh", "-c", fmt.Sprintf("convert %s out.png", filename))
|
|
138
|
+
|
|
139
|
+
// SAFE — argv form (Command's default)
|
|
140
|
+
cmd := exec.Command("convert", filename, "out.png")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Java — `ProcessBuilder` with array
|
|
144
|
+
|
|
145
|
+
```java
|
|
146
|
+
import java.lang.ProcessBuilder;
|
|
147
|
+
|
|
148
|
+
// UNSAFE — Runtime.exec(String) tokenizes via shell-like rules
|
|
149
|
+
Runtime.getRuntime().exec("convert " + filename + " out.png");
|
|
150
|
+
|
|
151
|
+
// SAFE — ProcessBuilder with explicit list
|
|
152
|
+
ProcessBuilder pb = new ProcessBuilder("convert", filename, "out.png");
|
|
153
|
+
Process p = pb.start();
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### PHP — `escapeshellarg` or `proc_open` with explicit argv
|
|
157
|
+
|
|
158
|
+
PHP's standard shell-invocation APIs (`system`, `exec`, `passthru`,
|
|
159
|
+
`shell_exec`) all go through the shell. Either escape every argument
|
|
160
|
+
or use `proc_open` with bypass_shell.
|
|
161
|
+
|
|
162
|
+
```php
|
|
163
|
+
// UNSAFE
|
|
164
|
+
system("convert $filename out.png");
|
|
165
|
+
|
|
166
|
+
// PARTIAL FIX — escapeshellarg quotes special chars
|
|
167
|
+
system("convert " . escapeshellarg($filename) . " out.png");
|
|
168
|
+
|
|
169
|
+
// SAFER — proc_open with bypass_shell
|
|
170
|
+
$descriptors = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
|
|
171
|
+
$process = proc_open(
|
|
172
|
+
['convert', $filename, 'out.png'], // PHP 7.4+ supports array form
|
|
173
|
+
$descriptors,
|
|
174
|
+
$pipes
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## False-positive patterns
|
|
179
|
+
|
|
180
|
+
Pre-validated allow-lists make the shell-string call safe by
|
|
181
|
+
construction:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
ALLOWED_OUTPUT_FORMATS = {"png", "jpg", "webp"}
|
|
185
|
+
if output_format not in ALLOWED_OUTPUT_FORMATS:
|
|
186
|
+
raise ValueError()
|
|
187
|
+
# `output_format` is now constrained to a known-safe set
|
|
188
|
+
subprocess.run(f"convert {filename} out.{output_format}", shell=True)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The `shell=True` is still flagged by the regex scanner, but the
|
|
192
|
+
finding is a false positive after allow-list validation. The
|
|
193
|
+
scanner can't reason about allow-list validation; the human
|
|
194
|
+
reader can.
|
|
195
|
+
|
|
196
|
+
That said: even with allow-list validation, the no-shell form is
|
|
197
|
+
strictly safer. There's no operational reason to keep the
|
|
198
|
+
shell-wrapper if the no-shell form works.
|
|
199
|
+
|
|
200
|
+
## Primary sources
|
|
201
|
+
|
|
202
|
+
- [CWE-78 Command Injection](https://cwe.mitre.org/data/definitions/78.html)
|
|
203
|
+
- [OWASP A03:2021 Injection](https://owasp.org/Top10/A03_2021-Injection/)
|
|
204
|
+
- [OWASP Command Injection Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html)
|
|
205
|
+
- [Python subprocess docs — Security considerations](https://docs.python.org/3/library/subprocess.html#security-considerations)
|
|
206
|
+
- [Node.js child_process docs](https://nodejs.org/api/child_process.html)
|