@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.
Files changed (112) hide show
  1. package/.claude-plugin/plugin.json +8 -3
  2. package/README.md +8 -0
  3. package/commands/pentest.md +5 -0
  4. package/package.json +8 -3
  5. package/skills/analyzing-tls-config/SKILL.md +221 -0
  6. package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
  7. package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
  8. package/skills/analyzing-tls-config/references/THEORY.md +128 -0
  9. package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
  10. package/skills/auditing-cors-policy/SKILL.md +186 -0
  11. package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
  12. package/skills/auditing-cors-policy/references/THEORY.md +142 -0
  13. package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
  14. package/skills/auditing-npm-dependencies/SKILL.md +254 -0
  15. package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
  16. package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
  17. package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
  18. package/skills/auditing-python-dependencies/SKILL.md +251 -0
  19. package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
  20. package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
  21. package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
  22. package/skills/checking-http-security-headers/SKILL.md +176 -0
  23. package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
  24. package/skills/checking-http-security-headers/references/THEORY.md +137 -0
  25. package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
  26. package/skills/checking-license-compliance/SKILL.md +225 -0
  27. package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
  28. package/skills/checking-license-compliance/references/THEORY.md +152 -0
  29. package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
  30. package/skills/composing-vulnerability-report/SKILL.md +212 -0
  31. package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
  32. package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
  33. package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
  34. package/skills/confirming-pentest-authorization/SKILL.md +247 -0
  35. package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
  36. package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
  37. package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
  38. package/skills/defining-pentest-scope/SKILL.md +227 -0
  39. package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
  40. package/skills/defining-pentest-scope/references/THEORY.md +170 -0
  41. package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
  42. package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
  43. package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
  44. package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
  45. package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
  46. package/skills/detecting-debug-endpoints/SKILL.md +207 -0
  47. package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
  48. package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
  49. package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
  50. package/skills/detecting-directory-listing/SKILL.md +206 -0
  51. package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
  52. package/skills/detecting-directory-listing/references/THEORY.md +203 -0
  53. package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
  54. package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
  55. package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
  56. package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
  57. package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
  58. package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
  59. package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
  60. package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
  61. package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
  62. package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
  63. package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
  64. package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
  65. package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
  66. package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
  67. package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
  68. package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
  69. package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
  70. package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
  71. package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
  72. package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
  73. package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
  74. package/skills/detecting-weak-cryptography/SKILL.md +147 -0
  75. package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
  76. package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
  77. package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
  78. package/skills/fingerprinting-server-software/SKILL.md +191 -0
  79. package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
  80. package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
  81. package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
  82. package/skills/generating-executive-summary/SKILL.md +261 -0
  83. package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
  84. package/skills/generating-executive-summary/references/THEORY.md +195 -0
  85. package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
  86. package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
  87. package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
  88. package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
  89. package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
  90. package/skills/performing-penetration-testing/SKILL.md +282 -190
  91. package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
  92. package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
  93. package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
  94. package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
  95. package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
  96. package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
  97. package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
  98. package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
  99. package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
  100. package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
  101. package/skills/recording-pentest-engagement/SKILL.md +253 -0
  102. package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
  103. package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
  104. package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
  105. package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
  106. package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
  107. package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
  108. package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
  109. package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
  110. package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
  111. package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
  112. 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)