@sikkuumiii/defuse 0.0.0-stage → 0.5.0
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/LICENSE +235 -0
- package/README.md +561 -2
- package/dist/src/cli.d.ts +20 -0
- package/dist/src/cli.js +619 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/core/analyze.d.ts +178 -0
- package/dist/src/core/analyze.js +834 -0
- package/dist/src/core/analyze.js.map +1 -0
- package/dist/src/core/coverage.d.ts +87 -0
- package/dist/src/core/coverage.js +181 -0
- package/dist/src/core/coverage.js.map +1 -0
- package/dist/src/core/finding.d.ts +196 -0
- package/dist/src/core/finding.js +178 -0
- package/dist/src/core/finding.js.map +1 -0
- package/dist/src/core/licence.d.ts +50 -0
- package/dist/src/core/licence.js +56 -0
- package/dist/src/core/licence.js.map +1 -0
- package/dist/src/core/paths.d.ts +21 -0
- package/dist/src/core/paths.js +58 -0
- package/dist/src/core/paths.js.map +1 -0
- package/dist/src/core/score.d.ts +137 -0
- package/dist/src/core/score.js +232 -0
- package/dist/src/core/score.js.map +1 -0
- package/dist/src/dast/targets.d.ts +30 -0
- package/dist/src/dast/targets.js +79 -0
- package/dist/src/dast/targets.js.map +1 -0
- package/dist/src/dast/zap.d.ts +42 -0
- package/dist/src/dast/zap.js +184 -0
- package/dist/src/dast/zap.js.map +1 -0
- package/dist/src/engine/scan.d.ts +80 -0
- package/dist/src/engine/scan.js +129 -0
- package/dist/src/engine/scan.js.map +1 -0
- package/dist/src/engine/shapes.d.ts +47 -0
- package/dist/src/engine/shapes.js +328 -0
- package/dist/src/engine/shapes.js.map +1 -0
- package/dist/src/engine/walk.d.ts +60 -0
- package/dist/src/engine/walk.js +156 -0
- package/dist/src/engine/walk.js.map +1 -0
- package/dist/src/parse/astDump.d.ts +31 -0
- package/dist/src/parse/astDump.js +66 -0
- package/dist/src/parse/astDump.js.map +1 -0
- package/dist/src/parse/languages.d.ts +70 -0
- package/dist/src/parse/languages.js +174 -0
- package/dist/src/parse/languages.js.map +1 -0
- package/dist/src/parse/location.d.ts +17 -0
- package/dist/src/parse/location.js +38 -0
- package/dist/src/parse/location.js.map +1 -0
- package/dist/src/parse/node-grammars.d.ts +12 -0
- package/dist/src/parse/node-grammars.js +35 -0
- package/dist/src/parse/node-grammars.js.map +1 -0
- package/dist/src/parse/parser.d.ts +126 -0
- package/dist/src/parse/parser.js +202 -0
- package/dist/src/parse/parser.js.map +1 -0
- package/dist/src/parse/tree-store.d.ts +139 -0
- package/dist/src/parse/tree-store.js +236 -0
- package/dist/src/parse/tree-store.js.map +1 -0
- package/dist/src/platform/dependency-rule.d.ts +27 -0
- package/dist/src/platform/dependency-rule.js +21 -0
- package/dist/src/platform/dependency-rule.js.map +1 -0
- package/dist/src/platform/deps.d.ts +86 -0
- package/dist/src/platform/deps.js +304 -0
- package/dist/src/platform/deps.js.map +1 -0
- package/dist/src/platform/php-facts.d.ts +49 -0
- package/dist/src/platform/php-facts.js +67 -0
- package/dist/src/platform/php-facts.js.map +1 -0
- package/dist/src/platform/runtime-version.d.ts +78 -0
- package/dist/src/platform/runtime-version.js +573 -0
- package/dist/src/platform/runtime-version.js.map +1 -0
- package/dist/src/report/colors.d.ts +50 -0
- package/dist/src/report/colors.js +97 -0
- package/dist/src/report/colors.js.map +1 -0
- package/dist/src/report/coverage-table.d.ts +90 -0
- package/dist/src/report/coverage-table.js +291 -0
- package/dist/src/report/coverage-table.js.map +1 -0
- package/dist/src/report/html-report.d.ts +66 -0
- package/dist/src/report/html-report.js +489 -0
- package/dist/src/report/html-report.js.map +1 -0
- package/dist/src/report/human.d.ts +44 -0
- package/dist/src/report/human.js +575 -0
- package/dist/src/report/human.js.map +1 -0
- package/dist/src/report/json.d.ts +24 -0
- package/dist/src/report/json.js +178 -0
- package/dist/src/report/json.js.map +1 -0
- package/dist/src/report/mark.d.ts +17 -0
- package/dist/src/report/mark.js +39 -0
- package/dist/src/report/mark.js.map +1 -0
- package/dist/src/report/pricing.d.ts +33 -0
- package/dist/src/report/pricing.js +343 -0
- package/dist/src/report/pricing.js.map +1 -0
- package/dist/src/report/sarif.d.ts +39 -0
- package/dist/src/report/sarif.js +219 -0
- package/dist/src/report/sarif.js.map +1 -0
- package/dist/src/report/site.d.ts +87 -0
- package/dist/src/report/site.js +742 -0
- package/dist/src/report/site.js.map +1 -0
- package/dist/src/rules/code-injection.d.ts +47 -0
- package/dist/src/rules/code-injection.js +326 -0
- package/dist/src/rules/code-injection.js.map +1 -0
- package/dist/src/rules/command-injection.d.ts +30 -0
- package/dist/src/rules/command-injection.js +286 -0
- package/dist/src/rules/command-injection.js.map +1 -0
- package/dist/src/rules/contract.d.ts +198 -0
- package/dist/src/rules/contract.js +39 -0
- package/dist/src/rules/contract.js.map +1 -0
- package/dist/src/rules/format-string.d.ts +46 -0
- package/dist/src/rules/format-string.js +146 -0
- package/dist/src/rules/format-string.js.map +1 -0
- package/dist/src/rules/hardcoded-secret.d.ts +38 -0
- package/dist/src/rules/hardcoded-secret.js +440 -0
- package/dist/src/rules/hardcoded-secret.js.map +1 -0
- package/dist/src/rules/lib/guards.d.ts +168 -0
- package/dist/src/rules/lib/guards.js +1179 -0
- package/dist/src/rules/lib/guards.js.map +1 -0
- package/dist/src/rules/lib/html-safety.d.ts +75 -0
- package/dist/src/rules/lib/html-safety.js +755 -0
- package/dist/src/rules/lib/html-safety.js.map +1 -0
- package/dist/src/rules/lib/local-build.d.ts +59 -0
- package/dist/src/rules/lib/local-build.js +320 -0
- package/dist/src/rules/lib/local-build.js.map +1 -0
- package/dist/src/rules/lib/strings.d.ts +208 -0
- package/dist/src/rules/lib/strings.js +1970 -0
- package/dist/src/rules/lib/strings.js.map +1 -0
- package/dist/src/rules/path-traversal.d.ts +33 -0
- package/dist/src/rules/path-traversal.js +96 -0
- package/dist/src/rules/path-traversal.js.map +1 -0
- package/dist/src/rules/registry.d.ts +19 -0
- package/dist/src/rules/registry.js +77 -0
- package/dist/src/rules/registry.js.map +1 -0
- package/dist/src/rules/sql-injection.d.ts +36 -0
- package/dist/src/rules/sql-injection.js +299 -0
- package/dist/src/rules/sql-injection.js.map +1 -0
- package/dist/src/rules/ssrf.d.ts +39 -0
- package/dist/src/rules/ssrf.js +111 -0
- package/dist/src/rules/ssrf.js.map +1 -0
- package/dist/src/rules/unbounded-copy.d.ts +37 -0
- package/dist/src/rules/unbounded-copy.js +147 -0
- package/dist/src/rules/unbounded-copy.js.map +1 -0
- package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
- package/dist/src/rules/unsafe-deserialization.js +276 -0
- package/dist/src/rules/unsafe-deserialization.js.map +1 -0
- package/dist/src/rules/weak-hash.d.ts +37 -0
- package/dist/src/rules/weak-hash.js +197 -0
- package/dist/src/rules/weak-hash.js.map +1 -0
- package/dist/src/rules/xss.d.ts +38 -0
- package/dist/src/rules/xss.js +527 -0
- package/dist/src/rules/xss.js.map +1 -0
- package/dist/src/taint/content-type.d.ts +19 -0
- package/dist/src/taint/content-type.js +20 -0
- package/dist/src/taint/content-type.js.map +1 -0
- package/dist/src/taint/dictionaries.d.ts +23 -0
- package/dist/src/taint/dictionaries.js +1778 -0
- package/dist/src/taint/dictionaries.js.map +1 -0
- package/dist/src/taint/fold.d.ts +59 -0
- package/dist/src/taint/fold.js +1177 -0
- package/dist/src/taint/fold.js.map +1 -0
- package/dist/src/taint/java-scope.d.ts +61 -0
- package/dist/src/taint/java-scope.js +212 -0
- package/dist/src/taint/java-scope.js.map +1 -0
- package/dist/src/taint/local-map.d.ts +79 -0
- package/dist/src/taint/local-map.js +603 -0
- package/dist/src/taint/local-map.js.map +1 -0
- package/dist/src/taint/project.d.ts +172 -0
- package/dist/src/taint/project.js +617 -0
- package/dist/src/taint/project.js.map +1 -0
- package/dist/src/taint/tracer.d.ts +265 -0
- package/dist/src/taint/tracer.js +3817 -0
- package/dist/src/taint/tracer.js.map +1 -0
- package/dist/src/taint/types.d.ts +484 -0
- package/dist/src/taint/types.js +47 -0
- package/dist/src/taint/types.js.map +1 -0
- package/dist/src/taint/visibility.d.ts +25 -0
- package/dist/src/taint/visibility.js +96 -0
- package/dist/src/taint/visibility.js.map +1 -0
- package/docs/examples/defuse.json +20 -0
- package/docs/examples/dvwa-urls.json +14 -0
- package/docs/examples/github-code-scanning.yml +54 -0
- package/docs/examples/tiny.js +4 -0
- package/package.json +73 -4
- package/src/cli.ts +747 -0
- package/src/core/analyze.ts +1072 -0
- package/src/core/coverage.ts +269 -0
- package/src/core/finding.ts +311 -0
- package/src/core/licence.ts +61 -0
- package/src/core/paths.ts +59 -0
- package/src/core/score.ts +353 -0
- package/src/dast/targets.ts +121 -0
- package/src/dast/zap.ts +246 -0
- package/src/engine/scan.ts +200 -0
- package/src/engine/shapes.ts +348 -0
- package/src/engine/walk.ts +196 -0
- package/src/parse/astDump.ts +85 -0
- package/src/parse/languages.ts +232 -0
- package/src/parse/location.ts +44 -0
- package/src/parse/node-grammars.ts +41 -0
- package/src/parse/parser.ts +262 -0
- package/src/parse/tree-store.ts +301 -0
- package/src/platform/dependency-rule.ts +33 -0
- package/src/platform/deps.ts +374 -0
- package/src/platform/php-facts.ts +85 -0
- package/src/platform/runtime-version.ts +710 -0
- package/src/report/colors.ts +107 -0
- package/src/report/coverage-table.ts +359 -0
- package/src/report/html-report.ts +601 -0
- package/src/report/human.ts +811 -0
- package/src/report/json.ts +189 -0
- package/src/report/mark.ts +47 -0
- package/src/report/pricing.ts +374 -0
- package/src/report/sarif.ts +235 -0
- package/src/report/site.ts +760 -0
- package/src/rules/code-injection.ts +346 -0
- package/src/rules/command-injection.ts +296 -0
- package/src/rules/contract.ts +220 -0
- package/src/rules/format-string.ts +158 -0
- package/src/rules/hardcoded-secret.ts +485 -0
- package/src/rules/lib/guards.ts +1235 -0
- package/src/rules/lib/html-safety.ts +793 -0
- package/src/rules/lib/local-build.ts +329 -0
- package/src/rules/lib/strings.ts +2034 -0
- package/src/rules/path-traversal.ts +101 -0
- package/src/rules/registry.ts +90 -0
- package/src/rules/sql-injection.ts +311 -0
- package/src/rules/ssrf.ts +118 -0
- package/src/rules/unbounded-copy.ts +162 -0
- package/src/rules/unsafe-deserialization.ts +291 -0
- package/src/rules/weak-hash.ts +226 -0
- package/src/rules/xss.ts +572 -0
- package/src/taint/content-type.ts +20 -0
- package/src/taint/dictionaries.ts +1855 -0
- package/src/taint/fold.ts +1206 -0
- package/src/taint/java-scope.ts +200 -0
- package/src/taint/local-map.ts +594 -0
- package/src/taint/project.ts +774 -0
- package/src/taint/tracer.ts +4298 -0
- package/src/taint/types.ts +511 -0
- package/src/taint/visibility.ts +100 -0
- package/tsconfig.json +61 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS COMMAND INJECTION (CWE-78, OWASP A05:2025 - Injection)
|
|
3
|
+
*
|
|
4
|
+
* WHAT THE BUG IS, in plain language:
|
|
5
|
+
* Some functions hand a string to the operating system's SHELL - the same
|
|
6
|
+
* program that runs your terminal. The shell has its own punctuation with
|
|
7
|
+
* special meaning: `;` ends a command and starts another, `|` pipes output,
|
|
8
|
+
* `&&` chains, and backticks or `$( )` run a command and paste its output.
|
|
9
|
+
*
|
|
10
|
+
* exec("ping -c 1 " + host)
|
|
11
|
+
*
|
|
12
|
+
* If `host` is `example.com; rm -rf /`, the shell sees TWO commands and runs
|
|
13
|
+
* both. Same disease as SQL injection: data got mixed into instructions.
|
|
14
|
+
*
|
|
15
|
+
* THE FIX:
|
|
16
|
+
* Don't use a shell. Every language has a way to launch a program with its
|
|
17
|
+
* arguments as a LIST rather than a sentence - `execFile`/`spawn` without
|
|
18
|
+
* `shell: true` in Node, `subprocess.run([...])` without `shell=True` in
|
|
19
|
+
* Python, `exec.Command("ping", "-c", "1", host)` in Go. When arguments are a
|
|
20
|
+
* list, `;` is just a character in a string. Nothing parses it.
|
|
21
|
+
*
|
|
22
|
+
* WHY THIS RULE IS FUSSIER THAN THE SQL ONE:
|
|
23
|
+
* `spawn("ls", [dir])` is safe, `spawn("ls " + dir, {shell:true})` is not, and
|
|
24
|
+
* they are the same function. Where the safety depends on an options object we
|
|
25
|
+
* check for it textually and say so; where we cannot tell, we stay quiet rather
|
|
26
|
+
* than guess. A scanner that cries wolf on every subprocess call gets muted,
|
|
27
|
+
* and a muted scanner finds nothing.
|
|
28
|
+
*/
|
|
29
|
+
import { asCall } from './contract.js';
|
|
30
|
+
import { analyzeStringExpression, constantProof } from './lib/strings.js';
|
|
31
|
+
import { followLocalBuild } from './lib/local-build.js';
|
|
32
|
+
import { partsAreGuarded } from './lib/guards.js';
|
|
33
|
+
/** Calls that ALWAYS go through a shell. Dynamic argument = report it. */
|
|
34
|
+
const SHELL_SINKS = {
|
|
35
|
+
javascript: ['exec', 'execSync'],
|
|
36
|
+
typescript: ['exec', 'execSync'],
|
|
37
|
+
python: ['system', 'popen', 'getoutput', 'getstatusoutput'],
|
|
38
|
+
java: ['exec'], // Runtime.getRuntime().exec(String) splits naively - still injectable
|
|
39
|
+
go: [],
|
|
40
|
+
// Every one of these hands the string to /bin/sh. `exec` is PHP's own
|
|
41
|
+
// function, unrelated to the JS/Java `exec` above - it shares the name only.
|
|
42
|
+
php: ['system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open', 'pcntl_exec'],
|
|
43
|
+
/*
|
|
44
|
+
* C's shell calls, and the cleanest sink list in this table.
|
|
45
|
+
*
|
|
46
|
+
* There is no ambiguity to scope away here. `system()` runs /bin/sh, always,
|
|
47
|
+
* and has done since the 1970s; it has no safe overload, no options object,
|
|
48
|
+
* and no parameterised form to be confused with - which is exactly the thing
|
|
49
|
+
* that makes the SQL and deserialisation lists so fiddly elsewhere.
|
|
50
|
+
*
|
|
51
|
+
* The `execlp`/`execvp` variants search PATH, and `execl`/`execv` do not,
|
|
52
|
+
* but neither family uses a shell: they take an argument VECTOR, so a
|
|
53
|
+
* semicolon in an argument is a semicolon, not a command separator. They are
|
|
54
|
+
* the CORRECT fix for a system() call and are deliberately absent from this
|
|
55
|
+
* list - see safe/c-format-constant.c, which asserts execv stays quiet.
|
|
56
|
+
*
|
|
57
|
+
* The one exception is execl with an explicit shell: `execlp("sh", "sh",
|
|
58
|
+
* "-c", tainted, ...)`. That IS a shell, so `execlp` and `execvp` appear in
|
|
59
|
+
* CONDITIONAL_SINKS below, gated on a shell program being visible.
|
|
60
|
+
*/
|
|
61
|
+
c: ['system', 'popen'],
|
|
62
|
+
cpp: ['system', 'popen'],
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Calls that MAY go through a shell depending on an option.
|
|
66
|
+
* We only report these when the shell-enabling option is visible in the call.
|
|
67
|
+
*/
|
|
68
|
+
const CONDITIONAL_SINKS = {
|
|
69
|
+
javascript: ['spawn', 'spawnSync', 'execFile', 'execFileSync'],
|
|
70
|
+
typescript: ['spawn', 'spawnSync', 'execFile', 'execFileSync'],
|
|
71
|
+
python: ['run', 'call', 'check_call', 'check_output', 'Popen'],
|
|
72
|
+
java: ['ProcessBuilder', 'start'],
|
|
73
|
+
go: ['Command', 'CommandContext'],
|
|
74
|
+
// PHP has no safe-by-default process API to make conditional, so this list is
|
|
75
|
+
// empty and every shell call above is reported unconditionally.
|
|
76
|
+
php: [],
|
|
77
|
+
// exec* takes an argument vector and is SAFE - unless the program it runs is
|
|
78
|
+
// itself a shell, which turns the next argument back into a command.
|
|
79
|
+
c: ['execl', 'execlp', 'execv', 'execvp', 'execle', 'execve'],
|
|
80
|
+
cpp: ['execl', 'execlp', 'execv', 'execvp', 'execle', 'execve'],
|
|
81
|
+
};
|
|
82
|
+
const SHELL_OPTION = {
|
|
83
|
+
javascript: /shell\s*:\s*true/,
|
|
84
|
+
typescript: /shell\s*:\s*true/,
|
|
85
|
+
python: /shell\s*=\s*True/,
|
|
86
|
+
java: /\b(sh|bash|cmd\.exe|powershell)\b/,
|
|
87
|
+
// Unused: PHP's CONDITIONAL_SINKS list is empty, so nothing consults this.
|
|
88
|
+
// It exists because the type demands an entry for every language, and an
|
|
89
|
+
// entry that never matches is more honest than one that pretends to.
|
|
90
|
+
php: /(?!)/,
|
|
91
|
+
// Only a shell PROGRAM makes exec.Command dangerous. `exec.Command("ping",
|
|
92
|
+
// "-c", "1", host)` is genuinely safe, so matching on "-c" alone would be a
|
|
93
|
+
// false positive on correct code.
|
|
94
|
+
go: /["'`](sh|bash|zsh|cmd\.exe|powershell)["'`]/,
|
|
95
|
+
// Same reasoning as Go: only a shell PROGRAM makes an exec* call injectable.
|
|
96
|
+
// `execv("/bin/ping", args)` with a tainted arg is genuinely safe and must
|
|
97
|
+
// stay quiet, so the program name is what this matches - not the "-c".
|
|
98
|
+
c: /["'](\/bin\/)?(sh|bash|zsh|ksh|dash)["']/,
|
|
99
|
+
cpp: /["'](\/bin\/)?(sh|bash|zsh|ksh|dash)["']/,
|
|
100
|
+
};
|
|
101
|
+
function nodeText(node) {
|
|
102
|
+
return (node.text ?? '').replace(/\s+/g, ' ').trim();
|
|
103
|
+
}
|
|
104
|
+
export const commandInjectionRule = {
|
|
105
|
+
id: 'command-injection',
|
|
106
|
+
name: 'Shell command built from a dynamic string',
|
|
107
|
+
cwe: 'CWE-78',
|
|
108
|
+
owasp: 'A05:2025 Injection',
|
|
109
|
+
severity: 'critical',
|
|
110
|
+
summary: 'A shell command is built from pieces; if any piece comes from outside, it can add a command of its own.',
|
|
111
|
+
fix: 'Run the program with an argument list and no shell, e.g. subprocess.run(["ls", path]) or execFile("ls", [path]).',
|
|
112
|
+
explanation: 'A shell reads punctuation like ; | && ` and $() as instructions. If a command ' +
|
|
113
|
+
'string is glued together from pieces, anything spliced in can end your command ' +
|
|
114
|
+
'and start a new one of the attacker\'s choosing, running as your process. ' +
|
|
115
|
+
'The fix is to launch the program with its arguments as a list instead of a ' +
|
|
116
|
+
'sentence, so no shell ever parses the input.',
|
|
117
|
+
limitations: 'UNVERIFIED (signature-based): we confirmed a shell-executing call receives a ' +
|
|
118
|
+
'dynamically built string - either built inside the call, or built in a local ' +
|
|
119
|
+
'variable earlier in the same function and followed back to it. We did NOT confirm ' +
|
|
120
|
+
'the spliced value is attacker-controlled. A command variable written inside an if, ' +
|
|
121
|
+
'a loop or a switch is not followed, and for list-style APIs we judged shell usage ' +
|
|
122
|
+
'from the visible text of the call only - an options object built elsewhere is ' +
|
|
123
|
+
'invisible to us. A command built right inside the call from values that are ' +
|
|
124
|
+
'PROVABLY fixed - by the same constant proof, and with the same limits, as the SQL ' +
|
|
125
|
+
'rule\'s, including a static Java helper in another file - is withdrawn and listed ' +
|
|
126
|
+
'as proved clean with the reason. Java only: in every other language this rule ' +
|
|
127
|
+
'does not use the proof and reports as before. A command assembled on an earlier ' +
|
|
128
|
+
'line does not get that proof either.',
|
|
129
|
+
shapes: ['call'],
|
|
130
|
+
support: {
|
|
131
|
+
javascript: {
|
|
132
|
+
status: 'implemented',
|
|
133
|
+
note: 'child_process exec/execSync always reported; spawn/execFile reported only when `shell: true` appears in the call text. A command built in a local variable before the call (`const cmd = "ls " + x; exec(cmd)`) is followed back to its build; one written inside a branch or loop, or passed in as a parameter, is not. An options object assembled in another variable is missed, because we read the shell option from the visible text of the call.',
|
|
134
|
+
},
|
|
135
|
+
typescript: {
|
|
136
|
+
status: 'implemented',
|
|
137
|
+
note: 'Identical to JavaScript.',
|
|
138
|
+
},
|
|
139
|
+
python: {
|
|
140
|
+
status: 'implemented',
|
|
141
|
+
note: 'os.system/os.popen always reported; subprocess.* reported only when `shell=True` is visible in the call. A command built in a local variable before the call is followed back to its build, including `+=` extensions after it; one written inside a branch or loop, or passed in as a parameter, is not.',
|
|
142
|
+
},
|
|
143
|
+
java: {
|
|
144
|
+
status: 'partial',
|
|
145
|
+
note: 'PARTIAL: Runtime.exec(String) and ProcessBuilder are detected when the command string is built at the call site, or in a straight-line assignment earlier in the same method. Java code commonly builds commands with StringBuilder across several statements, or picks the shell per operating system inside an if/else; the signature pass follows neither, and the taint engine tracks variables rather than the interior of objects, so a StringBuilder is not followed there either.',
|
|
146
|
+
},
|
|
147
|
+
go: {
|
|
148
|
+
status: 'partial',
|
|
149
|
+
note: 'PARTIAL: exec.Command is only reported when the call itself mentions a shell ("sh", "-c"). exec.Command("ping", host) is genuinely safe, so reporting every exec.Command would be noise - but that also means a shell wrapper built elsewhere is missed. The command string handed to `sh -c` is followed back to a local build (`c := "ls " + x`, fmt.Sprintf) when it was written in straight-line code.',
|
|
150
|
+
},
|
|
151
|
+
php: {
|
|
152
|
+
status: 'implemented',
|
|
153
|
+
note: 'Covers system, exec, shell_exec, passthru, popen, proc_open and pcntl_exec. Unlike Go and Java there is no conditional case to weigh: every one of these hands the string to a shell, so a dynamic argument is always reported.',
|
|
154
|
+
},
|
|
155
|
+
c: {
|
|
156
|
+
status: 'implemented',
|
|
157
|
+
note: 'system() and popen() hand the string to /bin/sh with no safe overload to be confused with, which makes this the least ambiguous sink list in the tool. exec* takes an argument VECTOR and is reported ONLY when the program being run is itself a shell - execv("/bin/ping", args) with a tainted argument is genuinely safe and stays quiet. The build-then-run idiom is followed through the buffer at both levels. The tracer follows a recognised source into sprintf/strcat and out through system(). The signature pass - which until build-then-run.c saw only `system(buf)`, a bare name, and so reported NOTHING for any C command injection whose source it did not recognise - now reads how the buffer was filled: sprintf/snprintf (reading the format string, so %d is not a splice and %s is), strcpy then strcat, an initialised array then strcat. A buffer written inside an if, a loop or a switch is not followed, and says nothing.',
|
|
158
|
+
},
|
|
159
|
+
cpp: {
|
|
160
|
+
status: 'implemented',
|
|
161
|
+
note: 'Every C entry applies unchanged, since C++ inherits the whole libc surface and real code still uses it. Namespace-qualified calls (std::system) and method calls on objects are recognised in addition. std::system() takes a const char*, so every C++ shell call ends in .c_str() or .data(); those are looked through, and a std::string built with +, += or append() before the call is followed back to its build - under the same straight-line-only condition as C.',
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
check(shape, ctx) {
|
|
165
|
+
const call = asCall(shape);
|
|
166
|
+
if (!call)
|
|
167
|
+
return null;
|
|
168
|
+
const language = ctx.language;
|
|
169
|
+
/*
|
|
170
|
+
* A SHELL FUNCTION THAT WAS RENAMED IS STILL A SHELL FUNCTION.
|
|
171
|
+
*
|
|
172
|
+
* import { exec } from "child_process";
|
|
173
|
+
* const execAsync = promisify(exec);
|
|
174
|
+
* await execAsync(command);
|
|
175
|
+
*
|
|
176
|
+
* That is CVE-2025-53107 - twenty-three vulnerable files - and this rule
|
|
177
|
+
* found none of them, because the list holds `exec` and the call says
|
|
178
|
+
* `execAsync`. The near-identical CVE-2025-59046 WAS caught, purely because
|
|
179
|
+
* that author wrote `const exec = promisify(execCb)` and aliased back to a
|
|
180
|
+
* name we already knew. Two CVEs, and the only difference was luck.
|
|
181
|
+
*
|
|
182
|
+
* `promisify` is the standard Node idiom for exactly these APIs, so the
|
|
183
|
+
* aliased form is the normal one, not an exotic one. File-local only: an
|
|
184
|
+
* alias exported from another module is still missed.
|
|
185
|
+
*/
|
|
186
|
+
const aliasOf = (called) => {
|
|
187
|
+
const declaration = new RegExp(`\\b${called.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*([^;\\n]+)`).exec(ctx.file.source);
|
|
188
|
+
if (!declaration?.[1])
|
|
189
|
+
return null;
|
|
190
|
+
const initialiser = declaration[1];
|
|
191
|
+
if (!/\bpromisify\s*\(|require\s*\(|child_process/.test(initialiser))
|
|
192
|
+
return null;
|
|
193
|
+
for (const known of [...SHELL_SINKS[language], ...CONDITIONAL_SINKS[language]]) {
|
|
194
|
+
if (known !== called && new RegExp(`\\b${known}\\b`).test(initialiser))
|
|
195
|
+
return known;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
const resolved = SHELL_SINKS[language].includes(call.calleeName) ||
|
|
200
|
+
CONDITIONAL_SINKS[language].includes(call.calleeName)
|
|
201
|
+
? call.calleeName
|
|
202
|
+
: (aliasOf(call.calleeName) ?? call.calleeName);
|
|
203
|
+
const always = SHELL_SINKS[language].includes(resolved);
|
|
204
|
+
const conditional = CONDITIONAL_SINKS[language].includes(resolved);
|
|
205
|
+
if (!always && !conditional)
|
|
206
|
+
return null;
|
|
207
|
+
// For the conditional family, require visible evidence that a shell is used.
|
|
208
|
+
const wholeCall = nodeText(call.node);
|
|
209
|
+
if (conditional && !always && !SHELL_OPTION[language].test(wholeCall))
|
|
210
|
+
return null;
|
|
211
|
+
for (const arg of call.args) {
|
|
212
|
+
let built = analyzeStringExpression(arg, language);
|
|
213
|
+
let builtOnLine = null;
|
|
214
|
+
/*
|
|
215
|
+
* A bare variable tells us nothing about a command ON ITS OWN - "you
|
|
216
|
+
* passed a variable to exec" is not actionable, and the rule has always
|
|
217
|
+
* skipped it. What it never did was ask the next question: where was that
|
|
218
|
+
* variable built? `cmd = "ls " + name` one line earlier is the same
|
|
219
|
+
* finding as `exec("ls " + name)`, and in C - which cannot build a string
|
|
220
|
+
* inside a call's brackets at all - it is the ONLY form a command
|
|
221
|
+
* injection ever takes. lib/local-build.ts says what it follows and,
|
|
222
|
+
* more importantly, where it gives up.
|
|
223
|
+
*/
|
|
224
|
+
if (built.mechanism === 'opaque' && built.literalText.length === 0) {
|
|
225
|
+
const followed = followLocalBuild(arg, language);
|
|
226
|
+
if (!followed)
|
|
227
|
+
continue;
|
|
228
|
+
built = followed.expression;
|
|
229
|
+
builtOnLine = followed.line;
|
|
230
|
+
}
|
|
231
|
+
if (!built.isDynamic)
|
|
232
|
+
continue;
|
|
233
|
+
if (built.mechanism === 'opaque' && built.literalText.length === 0)
|
|
234
|
+
continue;
|
|
235
|
+
/*
|
|
236
|
+
* ASSEMBLED FROM FIXED PIECES IS NOT ASSEMBLED FROM INPUT. The SQL rule
|
|
237
|
+
* has asked this since the constant proof existed; this rule never did,
|
|
238
|
+
* so `exec(cmd + bar)` with both halves provably fixed was reported -
|
|
239
|
+
* 31 of BenchmarkJava's command-injection false positives, where `cmd`
|
|
240
|
+
* comes from a helper in another file (StaticHelperCommand.java). When
|
|
241
|
+
* the proof needed reasoning it leaves a receipt, as the SQL rule's does.
|
|
242
|
+
*
|
|
243
|
+
* Only for a command written right here in the call. One assembled on an
|
|
244
|
+
* earlier line (builtOnLine) is read at the call, and a name's value at
|
|
245
|
+
* the call can differ from its value where the command was built - so
|
|
246
|
+
* that case keeps the guess.
|
|
247
|
+
*
|
|
248
|
+
* And only in Java for now. The first two rounds of an agent trying to
|
|
249
|
+
* break the proof (tests/probes/) found most of its holes outside Java, and the
|
|
250
|
+
* command rule had never leaned on it in any language; it starts where
|
|
251
|
+
* it has been tested hardest. Elsewhere this rule reports as before.
|
|
252
|
+
*/
|
|
253
|
+
if (builtOnLine === null && language === 'java') {
|
|
254
|
+
const proof = constantProof(arg, built.dynamicParts, language, { staticMethod: ctx.staticMethod, javaTypeInScan: ctx.javaTypeInScan });
|
|
255
|
+
if (proof) {
|
|
256
|
+
if (proof.reasoned)
|
|
257
|
+
ctx.noteClean?.('command-injection', arg, proof.reason);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/*
|
|
262
|
+
* A VALIDATION GUARD. Inside `if (is_numeric($octet[0]) && ...)` the value
|
|
263
|
+
* provably holds no shell metacharacter - it is digits. This is DVWA's own
|
|
264
|
+
* fix in exec/impossible.php, and reporting it was the seventh time this
|
|
265
|
+
* project punished a corrected file, and the last one still showing in the
|
|
266
|
+
* instrument panel. lib/guards.ts documents why the predicate list is tiny.
|
|
267
|
+
*/
|
|
268
|
+
if (partsAreGuarded(arg, built.dynamicParts, language, 'command'))
|
|
269
|
+
continue;
|
|
270
|
+
const callLine = arg.startPosition.row + 1;
|
|
271
|
+
const where = builtOnLine === null || builtOnLine === callLine ? '' : ` on line ${builtOnLine}`;
|
|
272
|
+
return {
|
|
273
|
+
node: arg,
|
|
274
|
+
message: `Command passed to ${call.calleeText}() is built with ${built.mechanism}${where}`,
|
|
275
|
+
reasoning: `\`${call.calleeText}()\` executes its argument through a shell` +
|
|
276
|
+
`${conditional && !always ? ' (a shell option is set in this call)' : ''}. ` +
|
|
277
|
+
`The command string is not fixed - it was assembled via ${built.mechanism}${where}, ` +
|
|
278
|
+
`splicing in ${built.dynamicParts.slice(0, 3).map((p) => `\`${p}\``).join(', ') || 'a runtime value'}. ` +
|
|
279
|
+
`Shell metacharacters (; | && \` $()) inside that value would be executed as ` +
|
|
280
|
+
`separate commands.`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
//# sourceMappingURL=command-injection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"command-injection.js","sourceRoot":"","sources":["../../../src/rules/command-injection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RULE CONTRACT
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* A "rule" is one thing Defuse knows how to look for. This file defines the shape
|
|
6
|
+
* every rule must have. Nothing else in the codebase knows what SQL injection
|
|
7
|
+
* is - the engine just runs whatever satisfies this interface.
|
|
8
|
+
*
|
|
9
|
+
* ARCHITECTURAL DECISION (this is the big one - it shapes Phases 3 and 5):
|
|
10
|
+
*
|
|
11
|
+
* Rules do NOT write tree-sitter queries and rules do NOT walk the AST.
|
|
12
|
+
* Instead, the engine lowers every language's syntax tree into a handful of
|
|
13
|
+
* language-neutral SHAPES - right now `CallSite` and `Assignment` - and a rule
|
|
14
|
+
* is a plain function that inspects a shape and says yes or no.
|
|
15
|
+
*
|
|
16
|
+
* Why:
|
|
17
|
+
* - A rule written once works for all five languages. `db.query(...)` in JS,
|
|
18
|
+
* `cursor.execute(...)` in Python and `stmt.executeQuery(...)` in Java all
|
|
19
|
+
* arrive at the rule as the same CallSite object with a different
|
|
20
|
+
* `calleeName`. The rule needs a name list per language, not a rewrite.
|
|
21
|
+
* - Adding language number six means teaching the SHAPE LAYER one new query.
|
|
22
|
+
* Every existing rule gains that language for free.
|
|
23
|
+
* - Phase 3's taint engine needs exactly these shapes anyway (a "source" is a
|
|
24
|
+
* CallSite, a "sink" is a CallSite, propagation happens through
|
|
25
|
+
* Assignments). Building them now means Phase 3 is an addition, not a
|
|
26
|
+
* rewrite.
|
|
27
|
+
*
|
|
28
|
+
* The escape hatch: a rule may declare `status: 'partial'` or
|
|
29
|
+
* `'not-implemented'` for a language the shape layer cannot express. That is a
|
|
30
|
+
* documented gap, printed in every report. It is never silence.
|
|
31
|
+
*/
|
|
32
|
+
import type { Node } from 'web-tree-sitter';
|
|
33
|
+
import type { Severity } from '../core/finding.js';
|
|
34
|
+
import type { LanguageId } from '../parse/languages.js';
|
|
35
|
+
import type { ParsedFile } from '../parse/parser.js';
|
|
36
|
+
/**
|
|
37
|
+
* A function/method call, normalised across languages.
|
|
38
|
+
*
|
|
39
|
+
* JS db.query(sql) receiverText "db" calleeName "query"
|
|
40
|
+
* Python cursor.execute(sql) receiverText "cursor" calleeName "execute"
|
|
41
|
+
* Java stmt.executeQuery(sql) receiverText "stmt" calleeName "executeQuery"
|
|
42
|
+
* Go db.Query(sql) receiverText "db" calleeName "Query"
|
|
43
|
+
*/
|
|
44
|
+
export interface CallSite {
|
|
45
|
+
readonly kind: 'call';
|
|
46
|
+
/** The whole call expression - what a finding points at. */
|
|
47
|
+
readonly node: Node;
|
|
48
|
+
/** The method or function name only, no dots. */
|
|
49
|
+
readonly calleeName: string;
|
|
50
|
+
/** Text before the dot, e.g. "db", "child_process", "Runtime.getRuntime()". */
|
|
51
|
+
readonly receiverText: string;
|
|
52
|
+
/** Full callee text, e.g. "crypto.createHash". */
|
|
53
|
+
readonly calleeText: string;
|
|
54
|
+
/** Argument expressions, already stripped of commas and parentheses. */
|
|
55
|
+
readonly args: readonly Node[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A value being stored somewhere, normalised across languages. Covers
|
|
59
|
+
* variable declarations, plain assignments, object/dict literal entries and
|
|
60
|
+
* keyword arguments - anywhere a name gets bound to a value.
|
|
61
|
+
*/
|
|
62
|
+
export interface Assignment {
|
|
63
|
+
readonly kind: 'assignment';
|
|
64
|
+
readonly node: Node;
|
|
65
|
+
/** Full left-hand side text, e.g. "el.innerHTML", "config.apiKey". */
|
|
66
|
+
readonly targetText: string;
|
|
67
|
+
/** Last segment of the target, e.g. "innerHTML", "apiKey". */
|
|
68
|
+
readonly targetName: string;
|
|
69
|
+
/** The value expression. */
|
|
70
|
+
readonly value: Node;
|
|
71
|
+
}
|
|
72
|
+
export type Shape = CallSite | Assignment;
|
|
73
|
+
/** Everything a rule is allowed to know while deciding. */
|
|
74
|
+
export interface RuleContext {
|
|
75
|
+
readonly file: ParsedFile;
|
|
76
|
+
readonly language: LanguageId;
|
|
77
|
+
/** Convenience: source text of a node, whitespace-collapsed. */
|
|
78
|
+
readonly text: (node: Node) => string;
|
|
79
|
+
/**
|
|
80
|
+
* Record that this rule examined a line and can PROVE it is not the bug the
|
|
81
|
+
* shape suggests - so the guess it would have made is withdrawn, not dropped.
|
|
82
|
+
*
|
|
83
|
+
* A rule that declines to report leaves no trace, and that was fine while
|
|
84
|
+
* declining meant "these are plain literals". Once a rule can decline because
|
|
85
|
+
* of REASONING - a branch that cannot run, a condition fixed at compile time -
|
|
86
|
+
* the reasoning has to be visible, because reasoning can be wrong. Receipts
|
|
87
|
+
* appear in --json as verifiedClean entries with the reason attached, are
|
|
88
|
+
* counted in the terminal summary, and show as "proved clean" in the UI.
|
|
89
|
+
*
|
|
90
|
+
* Optional so a caller that builds its own context is not broken by it.
|
|
91
|
+
*/
|
|
92
|
+
readonly noteClean?: (ruleId: string, node: Node, reason: string) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Java only: the static method a call like `Utils.m(...)` or
|
|
95
|
+
* `a.b.Utils.m(...)` runs, when that is not in doubt - see
|
|
96
|
+
* taint/project.ts, staticMethod(). Absent when cross-file resolution is
|
|
97
|
+
* off, and then nothing is read through another file.
|
|
98
|
+
*/
|
|
99
|
+
readonly staticMethod?: StaticMethodLookup | undefined;
|
|
100
|
+
/** Java only: is a type of this simple name - top-level or nested - declared anywhere in the scan? */
|
|
101
|
+
readonly javaTypeInScan?: ((simpleName: string) => boolean) | undefined;
|
|
102
|
+
}
|
|
103
|
+
/** A static method found in the scan: the file a reader should be shown, and its declaration. */
|
|
104
|
+
export interface StaticMethodHit {
|
|
105
|
+
readonly path: string;
|
|
106
|
+
readonly method: Node;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* `receiver` is the class as written at the call - `Utils` or `a.b.Utils`;
|
|
110
|
+
* `supertypes`, what the classes around the call inherit from.
|
|
111
|
+
*/
|
|
112
|
+
export type StaticMethodLookup = (receiver: string, method: string, supertypes?: readonly string[], allowInstance?: boolean) => StaticMethodHit | null;
|
|
113
|
+
/**
|
|
114
|
+
* What a rule returns when it decides something is worth reporting.
|
|
115
|
+
* Note there is NO confidence field: a rule cannot set its own confidence.
|
|
116
|
+
* The engine stamps a rule's result as signature-based; only the taint engine,
|
|
117
|
+
* which can produce a source-to-sink path, may emit flow-verified. A rule that wanted
|
|
118
|
+
* to claim more would have to change the engine, in a commit, in review.
|
|
119
|
+
*/
|
|
120
|
+
export interface RuleHit {
|
|
121
|
+
/** Node to point the finding at. Usually the shape's node or a sub-node. */
|
|
122
|
+
readonly node: Node;
|
|
123
|
+
/** One line for the terminal. */
|
|
124
|
+
readonly message: string;
|
|
125
|
+
/** Why this matched, in plain language. Shown to the user. Required. */
|
|
126
|
+
readonly reasoning: string;
|
|
127
|
+
/** Optional per-hit override of the rule's general limitations text. */
|
|
128
|
+
readonly limitations?: string;
|
|
129
|
+
/** Optional severity override (e.g. a confirmed AWS key beats a generic one). */
|
|
130
|
+
readonly severity?: Severity;
|
|
131
|
+
}
|
|
132
|
+
export type SupportStatus = 'implemented' | 'partial' | 'not-implemented';
|
|
133
|
+
export interface LanguageSupport {
|
|
134
|
+
readonly status: SupportStatus;
|
|
135
|
+
/**
|
|
136
|
+
* REQUIRED for every language, including implemented ones. For 'partial' and
|
|
137
|
+
* 'not-implemented' this must say exactly what is missing. This text is
|
|
138
|
+
* printed in the coverage section of every scan, so a gap can never be silent.
|
|
139
|
+
*/
|
|
140
|
+
readonly note: string;
|
|
141
|
+
}
|
|
142
|
+
export interface Rule {
|
|
143
|
+
/** Stable machine id, kebab-case. Used in --json, tests and suppressions. */
|
|
144
|
+
readonly id: string;
|
|
145
|
+
readonly name: string;
|
|
146
|
+
/** CWE identifier - the industry's shared catalogue of weakness types. */
|
|
147
|
+
readonly cwe: string;
|
|
148
|
+
/** OWASP Top 10 (2025) category. */
|
|
149
|
+
readonly owasp: string;
|
|
150
|
+
readonly severity: Severity;
|
|
151
|
+
/**
|
|
152
|
+
* 2-4 sentences explaining the VULNERABILITY (not the detection code) to
|
|
153
|
+
* someone who has never heard of it. Printed by `defuse rules --explain`.
|
|
154
|
+
*/
|
|
155
|
+
readonly explanation: string;
|
|
156
|
+
/**
|
|
157
|
+
* ONE plain sentence a beginner can read under a finding: what goes wrong,
|
|
158
|
+
* no jargon. The terminal prints it on every finding; `explanation` is the
|
|
159
|
+
* long version behind --full and `rules --explain`.
|
|
160
|
+
*/
|
|
161
|
+
readonly summary: string;
|
|
162
|
+
/** ONE sentence: what to change in the code. Printed as `fix` on every finding. */
|
|
163
|
+
readonly fix: string;
|
|
164
|
+
/**
|
|
165
|
+
* What this rule structurally cannot determine. Copied onto every finding it
|
|
166
|
+
* produces unless the hit overrides it. Required - a rule with no stated
|
|
167
|
+
* limitations is a rule pretending to be certain.
|
|
168
|
+
*/
|
|
169
|
+
readonly limitations: string;
|
|
170
|
+
/**
|
|
171
|
+
* Per-language honesty declaration. REQUIRED FOR EVERY LANGUAGE.
|
|
172
|
+
*
|
|
173
|
+
* This was `Partial<Record<...>>` with "missing language = not-implemented"
|
|
174
|
+
* as the default, and that default caused the worst kind of bug this project
|
|
175
|
+
* can have: the tool contradicting itself about its own coverage.
|
|
176
|
+
*
|
|
177
|
+
* Adding C and C++ silently marked all eight existing rules not-implemented
|
|
178
|
+
* for them. rulesForLanguage() then filtered those rules out, so their
|
|
179
|
+
* SIGNATURE matchers never ran on a C file at all - while the taint engine,
|
|
180
|
+
* which reaches rules by id and never consults this table, went on producing
|
|
181
|
+
* flow-verified `command-injection` findings in C. The report printed six of
|
|
182
|
+
* them directly underneath a line saying command-injection/c was NOT
|
|
183
|
+
* IMPLEMENTED.
|
|
184
|
+
*
|
|
185
|
+
* A silent default is what made that possible. Requiring every language
|
|
186
|
+
* turns the next language addition into a compile error per rule, which is
|
|
187
|
+
* exactly what the taint dictionaries already do - nineteen of them, each a
|
|
188
|
+
* real decision someone had to make on purpose.
|
|
189
|
+
*/
|
|
190
|
+
readonly support: Record<LanguageId, LanguageSupport>;
|
|
191
|
+
/** Which shapes this rule wants to see. Engine only calls matching ones. */
|
|
192
|
+
readonly shapes: readonly Shape['kind'][];
|
|
193
|
+
/** The decision. Return null for "not interesting". */
|
|
194
|
+
readonly check: (shape: Shape, ctx: RuleContext) => RuleHit | null;
|
|
195
|
+
}
|
|
196
|
+
/** Helper so a rule can narrow a Shape without repeating the same three lines. */
|
|
197
|
+
export declare function asCall(shape: Shape): CallSite | null;
|
|
198
|
+
export declare function asAssignment(shape: Shape): Assignment | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE RULE CONTRACT
|
|
3
|
+
* =================
|
|
4
|
+
*
|
|
5
|
+
* A "rule" is one thing Defuse knows how to look for. This file defines the shape
|
|
6
|
+
* every rule must have. Nothing else in the codebase knows what SQL injection
|
|
7
|
+
* is - the engine just runs whatever satisfies this interface.
|
|
8
|
+
*
|
|
9
|
+
* ARCHITECTURAL DECISION (this is the big one - it shapes Phases 3 and 5):
|
|
10
|
+
*
|
|
11
|
+
* Rules do NOT write tree-sitter queries and rules do NOT walk the AST.
|
|
12
|
+
* Instead, the engine lowers every language's syntax tree into a handful of
|
|
13
|
+
* language-neutral SHAPES - right now `CallSite` and `Assignment` - and a rule
|
|
14
|
+
* is a plain function that inspects a shape and says yes or no.
|
|
15
|
+
*
|
|
16
|
+
* Why:
|
|
17
|
+
* - A rule written once works for all five languages. `db.query(...)` in JS,
|
|
18
|
+
* `cursor.execute(...)` in Python and `stmt.executeQuery(...)` in Java all
|
|
19
|
+
* arrive at the rule as the same CallSite object with a different
|
|
20
|
+
* `calleeName`. The rule needs a name list per language, not a rewrite.
|
|
21
|
+
* - Adding language number six means teaching the SHAPE LAYER one new query.
|
|
22
|
+
* Every existing rule gains that language for free.
|
|
23
|
+
* - Phase 3's taint engine needs exactly these shapes anyway (a "source" is a
|
|
24
|
+
* CallSite, a "sink" is a CallSite, propagation happens through
|
|
25
|
+
* Assignments). Building them now means Phase 3 is an addition, not a
|
|
26
|
+
* rewrite.
|
|
27
|
+
*
|
|
28
|
+
* The escape hatch: a rule may declare `status: 'partial'` or
|
|
29
|
+
* `'not-implemented'` for a language the shape layer cannot express. That is a
|
|
30
|
+
* documented gap, printed in every report. It is never silence.
|
|
31
|
+
*/
|
|
32
|
+
/** Helper so a rule can narrow a Shape without repeating the same three lines. */
|
|
33
|
+
export function asCall(shape) {
|
|
34
|
+
return shape.kind === 'call' ? shape : null;
|
|
35
|
+
}
|
|
36
|
+
export function asAssignment(shape) {
|
|
37
|
+
return shape.kind === 'assignment' ? shape : null;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../../src/rules/contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAsLH,kFAAkF;AAClF,MAAM,UAAU,MAAM,CAAC,KAAY;IACjC,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,KAAY;IACvC,OAAO,KAAK,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FORMAT STRING INJECTION (CWE-134)
|
|
3
|
+
*
|
|
4
|
+
* WHAT THE BUG IS.
|
|
5
|
+
*
|
|
6
|
+
* printf(name); // `name` came from the user
|
|
7
|
+
*
|
|
8
|
+
* looks like printing a string and is not. printf reads its first argument as a
|
|
9
|
+
* FORMAT, so whoever controls that string controls the conversions too:
|
|
10
|
+
*
|
|
11
|
+
* %x %x %x %x walks up the stack and prints whatever is sitting there
|
|
12
|
+
* %s treats a stack value as a pointer - crash, or a leak
|
|
13
|
+
* %n WRITES the number of bytes printed so far to a pointer
|
|
14
|
+
* argument, which turns a logging call into an arbitrary
|
|
15
|
+
* memory write
|
|
16
|
+
*
|
|
17
|
+
* That last conversion is why this is not a cosmetic bug. It is the standard
|
|
18
|
+
* route from "the program logs a username" to "the attacker runs code".
|
|
19
|
+
*
|
|
20
|
+
* WHY THIS RULE IS THE BEST ARGUMENT FOR POINTING A TAINT ENGINE AT C.
|
|
21
|
+
*
|
|
22
|
+
* Most famous C bugs are SIZE bugs - is this buffer big enough, is this integer
|
|
23
|
+
* about to wrap - and answering those needs an analysis of sizes, allocations
|
|
24
|
+
* and aliasing that this engine does not have. The unbounded-copy rule next
|
|
25
|
+
* door is careful to claim much less than its name suggests for exactly that
|
|
26
|
+
* reason.
|
|
27
|
+
*
|
|
28
|
+
* A format string bug needs none of it. The entire question is "is this ONE
|
|
29
|
+
* argument attacker-controlled?", which is the only question this engine
|
|
30
|
+
* answers, and it answers it with a printed path. So a flow-verified finding
|
|
31
|
+
* here means precisely what it says, with no hedge underneath it.
|
|
32
|
+
*
|
|
33
|
+
* THE TRAP THIS RULE MUST NOT FALL INTO.
|
|
34
|
+
*
|
|
35
|
+
* printf(name) dangerous
|
|
36
|
+
* printf("%s", name) correct
|
|
37
|
+
*
|
|
38
|
+
* Same function, one extra argument - the identical shape as a parameterised
|
|
39
|
+
* SQL query versus a concatenated one. A rule that fired on "printf with a
|
|
40
|
+
* tainted argument" would report the FIX as the bug, which this project treats
|
|
41
|
+
* as worse than a miss: it teaches people that hardening does not help. Only
|
|
42
|
+
* the format argument counts, and its index differs per function, so the table
|
|
43
|
+
* below is the whole rule.
|
|
44
|
+
*/
|
|
45
|
+
import { type Rule } from './contract.js';
|
|
46
|
+
export declare const formatStringRule: Rule;
|