@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.
Files changed (236) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +561 -2
  3. package/dist/src/cli.d.ts +20 -0
  4. package/dist/src/cli.js +619 -0
  5. package/dist/src/cli.js.map +1 -0
  6. package/dist/src/core/analyze.d.ts +178 -0
  7. package/dist/src/core/analyze.js +834 -0
  8. package/dist/src/core/analyze.js.map +1 -0
  9. package/dist/src/core/coverage.d.ts +87 -0
  10. package/dist/src/core/coverage.js +181 -0
  11. package/dist/src/core/coverage.js.map +1 -0
  12. package/dist/src/core/finding.d.ts +196 -0
  13. package/dist/src/core/finding.js +178 -0
  14. package/dist/src/core/finding.js.map +1 -0
  15. package/dist/src/core/licence.d.ts +50 -0
  16. package/dist/src/core/licence.js +56 -0
  17. package/dist/src/core/licence.js.map +1 -0
  18. package/dist/src/core/paths.d.ts +21 -0
  19. package/dist/src/core/paths.js +58 -0
  20. package/dist/src/core/paths.js.map +1 -0
  21. package/dist/src/core/score.d.ts +137 -0
  22. package/dist/src/core/score.js +232 -0
  23. package/dist/src/core/score.js.map +1 -0
  24. package/dist/src/dast/targets.d.ts +30 -0
  25. package/dist/src/dast/targets.js +79 -0
  26. package/dist/src/dast/targets.js.map +1 -0
  27. package/dist/src/dast/zap.d.ts +42 -0
  28. package/dist/src/dast/zap.js +184 -0
  29. package/dist/src/dast/zap.js.map +1 -0
  30. package/dist/src/engine/scan.d.ts +80 -0
  31. package/dist/src/engine/scan.js +129 -0
  32. package/dist/src/engine/scan.js.map +1 -0
  33. package/dist/src/engine/shapes.d.ts +47 -0
  34. package/dist/src/engine/shapes.js +328 -0
  35. package/dist/src/engine/shapes.js.map +1 -0
  36. package/dist/src/engine/walk.d.ts +60 -0
  37. package/dist/src/engine/walk.js +156 -0
  38. package/dist/src/engine/walk.js.map +1 -0
  39. package/dist/src/parse/astDump.d.ts +31 -0
  40. package/dist/src/parse/astDump.js +66 -0
  41. package/dist/src/parse/astDump.js.map +1 -0
  42. package/dist/src/parse/languages.d.ts +70 -0
  43. package/dist/src/parse/languages.js +174 -0
  44. package/dist/src/parse/languages.js.map +1 -0
  45. package/dist/src/parse/location.d.ts +17 -0
  46. package/dist/src/parse/location.js +38 -0
  47. package/dist/src/parse/location.js.map +1 -0
  48. package/dist/src/parse/node-grammars.d.ts +12 -0
  49. package/dist/src/parse/node-grammars.js +35 -0
  50. package/dist/src/parse/node-grammars.js.map +1 -0
  51. package/dist/src/parse/parser.d.ts +126 -0
  52. package/dist/src/parse/parser.js +202 -0
  53. package/dist/src/parse/parser.js.map +1 -0
  54. package/dist/src/parse/tree-store.d.ts +139 -0
  55. package/dist/src/parse/tree-store.js +236 -0
  56. package/dist/src/parse/tree-store.js.map +1 -0
  57. package/dist/src/platform/dependency-rule.d.ts +27 -0
  58. package/dist/src/platform/dependency-rule.js +21 -0
  59. package/dist/src/platform/dependency-rule.js.map +1 -0
  60. package/dist/src/platform/deps.d.ts +86 -0
  61. package/dist/src/platform/deps.js +304 -0
  62. package/dist/src/platform/deps.js.map +1 -0
  63. package/dist/src/platform/php-facts.d.ts +49 -0
  64. package/dist/src/platform/php-facts.js +67 -0
  65. package/dist/src/platform/php-facts.js.map +1 -0
  66. package/dist/src/platform/runtime-version.d.ts +78 -0
  67. package/dist/src/platform/runtime-version.js +573 -0
  68. package/dist/src/platform/runtime-version.js.map +1 -0
  69. package/dist/src/report/colors.d.ts +50 -0
  70. package/dist/src/report/colors.js +97 -0
  71. package/dist/src/report/colors.js.map +1 -0
  72. package/dist/src/report/coverage-table.d.ts +90 -0
  73. package/dist/src/report/coverage-table.js +291 -0
  74. package/dist/src/report/coverage-table.js.map +1 -0
  75. package/dist/src/report/html-report.d.ts +66 -0
  76. package/dist/src/report/html-report.js +489 -0
  77. package/dist/src/report/html-report.js.map +1 -0
  78. package/dist/src/report/human.d.ts +44 -0
  79. package/dist/src/report/human.js +575 -0
  80. package/dist/src/report/human.js.map +1 -0
  81. package/dist/src/report/json.d.ts +24 -0
  82. package/dist/src/report/json.js +178 -0
  83. package/dist/src/report/json.js.map +1 -0
  84. package/dist/src/report/mark.d.ts +17 -0
  85. package/dist/src/report/mark.js +39 -0
  86. package/dist/src/report/mark.js.map +1 -0
  87. package/dist/src/report/pricing.d.ts +33 -0
  88. package/dist/src/report/pricing.js +343 -0
  89. package/dist/src/report/pricing.js.map +1 -0
  90. package/dist/src/report/sarif.d.ts +39 -0
  91. package/dist/src/report/sarif.js +219 -0
  92. package/dist/src/report/sarif.js.map +1 -0
  93. package/dist/src/report/site.d.ts +87 -0
  94. package/dist/src/report/site.js +742 -0
  95. package/dist/src/report/site.js.map +1 -0
  96. package/dist/src/rules/code-injection.d.ts +47 -0
  97. package/dist/src/rules/code-injection.js +326 -0
  98. package/dist/src/rules/code-injection.js.map +1 -0
  99. package/dist/src/rules/command-injection.d.ts +30 -0
  100. package/dist/src/rules/command-injection.js +286 -0
  101. package/dist/src/rules/command-injection.js.map +1 -0
  102. package/dist/src/rules/contract.d.ts +198 -0
  103. package/dist/src/rules/contract.js +39 -0
  104. package/dist/src/rules/contract.js.map +1 -0
  105. package/dist/src/rules/format-string.d.ts +46 -0
  106. package/dist/src/rules/format-string.js +146 -0
  107. package/dist/src/rules/format-string.js.map +1 -0
  108. package/dist/src/rules/hardcoded-secret.d.ts +38 -0
  109. package/dist/src/rules/hardcoded-secret.js +440 -0
  110. package/dist/src/rules/hardcoded-secret.js.map +1 -0
  111. package/dist/src/rules/lib/guards.d.ts +168 -0
  112. package/dist/src/rules/lib/guards.js +1179 -0
  113. package/dist/src/rules/lib/guards.js.map +1 -0
  114. package/dist/src/rules/lib/html-safety.d.ts +75 -0
  115. package/dist/src/rules/lib/html-safety.js +755 -0
  116. package/dist/src/rules/lib/html-safety.js.map +1 -0
  117. package/dist/src/rules/lib/local-build.d.ts +59 -0
  118. package/dist/src/rules/lib/local-build.js +320 -0
  119. package/dist/src/rules/lib/local-build.js.map +1 -0
  120. package/dist/src/rules/lib/strings.d.ts +208 -0
  121. package/dist/src/rules/lib/strings.js +1970 -0
  122. package/dist/src/rules/lib/strings.js.map +1 -0
  123. package/dist/src/rules/path-traversal.d.ts +33 -0
  124. package/dist/src/rules/path-traversal.js +96 -0
  125. package/dist/src/rules/path-traversal.js.map +1 -0
  126. package/dist/src/rules/registry.d.ts +19 -0
  127. package/dist/src/rules/registry.js +77 -0
  128. package/dist/src/rules/registry.js.map +1 -0
  129. package/dist/src/rules/sql-injection.d.ts +36 -0
  130. package/dist/src/rules/sql-injection.js +299 -0
  131. package/dist/src/rules/sql-injection.js.map +1 -0
  132. package/dist/src/rules/ssrf.d.ts +39 -0
  133. package/dist/src/rules/ssrf.js +111 -0
  134. package/dist/src/rules/ssrf.js.map +1 -0
  135. package/dist/src/rules/unbounded-copy.d.ts +37 -0
  136. package/dist/src/rules/unbounded-copy.js +147 -0
  137. package/dist/src/rules/unbounded-copy.js.map +1 -0
  138. package/dist/src/rules/unsafe-deserialization.d.ts +43 -0
  139. package/dist/src/rules/unsafe-deserialization.js +276 -0
  140. package/dist/src/rules/unsafe-deserialization.js.map +1 -0
  141. package/dist/src/rules/weak-hash.d.ts +37 -0
  142. package/dist/src/rules/weak-hash.js +197 -0
  143. package/dist/src/rules/weak-hash.js.map +1 -0
  144. package/dist/src/rules/xss.d.ts +38 -0
  145. package/dist/src/rules/xss.js +527 -0
  146. package/dist/src/rules/xss.js.map +1 -0
  147. package/dist/src/taint/content-type.d.ts +19 -0
  148. package/dist/src/taint/content-type.js +20 -0
  149. package/dist/src/taint/content-type.js.map +1 -0
  150. package/dist/src/taint/dictionaries.d.ts +23 -0
  151. package/dist/src/taint/dictionaries.js +1778 -0
  152. package/dist/src/taint/dictionaries.js.map +1 -0
  153. package/dist/src/taint/fold.d.ts +59 -0
  154. package/dist/src/taint/fold.js +1177 -0
  155. package/dist/src/taint/fold.js.map +1 -0
  156. package/dist/src/taint/java-scope.d.ts +61 -0
  157. package/dist/src/taint/java-scope.js +212 -0
  158. package/dist/src/taint/java-scope.js.map +1 -0
  159. package/dist/src/taint/local-map.d.ts +79 -0
  160. package/dist/src/taint/local-map.js +603 -0
  161. package/dist/src/taint/local-map.js.map +1 -0
  162. package/dist/src/taint/project.d.ts +172 -0
  163. package/dist/src/taint/project.js +617 -0
  164. package/dist/src/taint/project.js.map +1 -0
  165. package/dist/src/taint/tracer.d.ts +265 -0
  166. package/dist/src/taint/tracer.js +3817 -0
  167. package/dist/src/taint/tracer.js.map +1 -0
  168. package/dist/src/taint/types.d.ts +484 -0
  169. package/dist/src/taint/types.js +47 -0
  170. package/dist/src/taint/types.js.map +1 -0
  171. package/dist/src/taint/visibility.d.ts +25 -0
  172. package/dist/src/taint/visibility.js +96 -0
  173. package/dist/src/taint/visibility.js.map +1 -0
  174. package/docs/examples/defuse.json +20 -0
  175. package/docs/examples/dvwa-urls.json +14 -0
  176. package/docs/examples/github-code-scanning.yml +54 -0
  177. package/docs/examples/tiny.js +4 -0
  178. package/package.json +73 -4
  179. package/src/cli.ts +747 -0
  180. package/src/core/analyze.ts +1072 -0
  181. package/src/core/coverage.ts +269 -0
  182. package/src/core/finding.ts +311 -0
  183. package/src/core/licence.ts +61 -0
  184. package/src/core/paths.ts +59 -0
  185. package/src/core/score.ts +353 -0
  186. package/src/dast/targets.ts +121 -0
  187. package/src/dast/zap.ts +246 -0
  188. package/src/engine/scan.ts +200 -0
  189. package/src/engine/shapes.ts +348 -0
  190. package/src/engine/walk.ts +196 -0
  191. package/src/parse/astDump.ts +85 -0
  192. package/src/parse/languages.ts +232 -0
  193. package/src/parse/location.ts +44 -0
  194. package/src/parse/node-grammars.ts +41 -0
  195. package/src/parse/parser.ts +262 -0
  196. package/src/parse/tree-store.ts +301 -0
  197. package/src/platform/dependency-rule.ts +33 -0
  198. package/src/platform/deps.ts +374 -0
  199. package/src/platform/php-facts.ts +85 -0
  200. package/src/platform/runtime-version.ts +710 -0
  201. package/src/report/colors.ts +107 -0
  202. package/src/report/coverage-table.ts +359 -0
  203. package/src/report/html-report.ts +601 -0
  204. package/src/report/human.ts +811 -0
  205. package/src/report/json.ts +189 -0
  206. package/src/report/mark.ts +47 -0
  207. package/src/report/pricing.ts +374 -0
  208. package/src/report/sarif.ts +235 -0
  209. package/src/report/site.ts +760 -0
  210. package/src/rules/code-injection.ts +346 -0
  211. package/src/rules/command-injection.ts +296 -0
  212. package/src/rules/contract.ts +220 -0
  213. package/src/rules/format-string.ts +158 -0
  214. package/src/rules/hardcoded-secret.ts +485 -0
  215. package/src/rules/lib/guards.ts +1235 -0
  216. package/src/rules/lib/html-safety.ts +793 -0
  217. package/src/rules/lib/local-build.ts +329 -0
  218. package/src/rules/lib/strings.ts +2034 -0
  219. package/src/rules/path-traversal.ts +101 -0
  220. package/src/rules/registry.ts +90 -0
  221. package/src/rules/sql-injection.ts +311 -0
  222. package/src/rules/ssrf.ts +118 -0
  223. package/src/rules/unbounded-copy.ts +162 -0
  224. package/src/rules/unsafe-deserialization.ts +291 -0
  225. package/src/rules/weak-hash.ts +226 -0
  226. package/src/rules/xss.ts +572 -0
  227. package/src/taint/content-type.ts +20 -0
  228. package/src/taint/dictionaries.ts +1855 -0
  229. package/src/taint/fold.ts +1206 -0
  230. package/src/taint/java-scope.ts +200 -0
  231. package/src/taint/local-map.ts +594 -0
  232. package/src/taint/project.ts +774 -0
  233. package/src/taint/tracer.ts +4298 -0
  234. package/src/taint/types.ts +511 -0
  235. package/src/taint/visibility.ts +100 -0
  236. package/tsconfig.json +61 -0
@@ -0,0 +1,101 @@
1
+ /**
2
+ * PATH TRAVERSAL (CWE-22, OWASP A01:2025 - Broken Access Control)
3
+ *
4
+ * WHAT THE BUG IS, in plain language:
5
+ * The server opens a file whose name the request chose.
6
+ *
7
+ * res.sendFile(path.join(__dirname, 'uploads', req.query.name));
8
+ *
9
+ * The code means "a file in uploads". The request sends
10
+ * `../../../etc/passwd`, and `..` walks up one folder per step until it is
11
+ * out of uploads and anywhere the server process can read - its own source,
12
+ * its `.env`, the keys on the box. On a write or a delete it is worse.
13
+ *
14
+ * THE FIX: keep only the file name (`path.basename`, `os.path.basename`,
15
+ * `secure_filename`, `filepath.Base`, `Path.getFileName`), or resolve the
16
+ * full path and check that it still starts with the folder you meant. Or
17
+ * hand the folder to an API that refuses to leave it (Express `sendFile`
18
+ * with `root`, Flask `send_from_directory`).
19
+ *
20
+ * ============================================================================
21
+ * WHY THIS RULE HAS NO SIGNATURE PASS
22
+ * ============================================================================
23
+ * Same reason as SSRF: "this code opens a file" is what programs do. Only a
24
+ * traced path from a request value to the open is a finding, so `check()`
25
+ * returns null and the taint engine emits every one.
26
+ *
27
+ * A path from the COMMAND LINE, the environment or the terminal is chosen by
28
+ * whoever runs the program, who could open that file themselves. It is
29
+ * reported low, as to review, not proven: it matters only when the program
30
+ * runs with more rights than its caller (setuid, a service, a CGI script).
31
+ */
32
+
33
+ import { type Rule, type RuleHit } from './contract.js';
34
+
35
+ export const pathTraversalRule: Rule = {
36
+ id: 'path-traversal',
37
+ name: 'File path the user chose',
38
+ cwe: 'CWE-22',
39
+ owasp: 'A01:2025 Broken Access Control',
40
+ severity: 'high',
41
+ summary: 'The server opens a file path the request may choose, so `../` could reach files outside the folder the code meant.',
42
+ fix:
43
+ 'Keep only the file name (basename / secure_filename / filepath.Base), or resolve the path and check it still starts with the intended folder.',
44
+ explanation:
45
+ 'The code means "a file in this folder", but the name comes from the request, ' +
46
+ 'and every `../` in it climbs one folder up. Enough of them and the server reads, ' +
47
+ 'writes or deletes any file its process can reach: its own source, its secrets, ' +
48
+ 'the keys on the machine. Keep only the last name in the path, or resolve the ' +
49
+ 'full path and check it still starts with the folder you meant before opening it.',
50
+ limitations:
51
+ 'FLOW-VERIFIED ONLY: "opens a file" describes almost every program, so there is no ' +
52
+ 'pattern pass and every finding comes from a traced path. A path from the command ' +
53
+ 'line, the environment or the terminal is chosen by whoever runs the program and is ' +
54
+ 'reported low, to review. A path opened after a prefix check (`resolved.startsWith(base)`) ' +
55
+ 'or a `..` blocklist in the same function is reported to review, never proven and never ' +
56
+ 'cleared: what the check compares is NOT read, so a correct one and a wrong one look the ' +
57
+ 'same here. path.join, path.resolve and filepath.Clean are NOT treated as fixes, because ' +
58
+ 'none of them stops `../`. A "not found" answer before the open (`if (!x) return res.status(404)`, ' +
59
+ 'abort(404), Http404) is read as a possible lookup of known names and also gives to review: ' +
60
+ 'what the lookup accepts is NOT read, and any 404 above the open in the function counts, ' +
61
+ 'even one about a different value.',
62
+ shapes: ['call'],
63
+ support: {
64
+ javascript: {
65
+ status: 'implemented',
66
+ note: 'Data-flow only. Covers fs / fs.promises / fs-extra reads, writes, deletes and directory listings, destructured readFileSync-style names, and Express res.sendFile / res.download without a root option. NOT covered: a bare readFile() whose import we cannot see.',
67
+ },
68
+ typescript: {
69
+ status: 'implemented',
70
+ note: 'Data-flow only. Identical to JavaScript.',
71
+ },
72
+ python: {
73
+ status: 'implemented',
74
+ note: 'Data-flow only. Covers open(), os file and directory calls, shutil copy / move / rmtree, and Flask send_file. NOT covered: pathlib (Path(x).read_text()), where the path lives in an object the tracer does not follow.',
75
+ },
76
+ java: {
77
+ status: 'implemented',
78
+ note: 'Data-flow only. Covers new File / FileInputStream / FileOutputStream / FileReader / FileWriter / RandomAccessFile / FileSystemResource and the java.nio Files calls that open a path (readAllBytes, newInputStream, write, delete, ...). Paths.get / Path.of only build a path and are followed, not reported. NOT covered: a Path held in a field and opened in another method.',
79
+ },
80
+ php: {
81
+ status: 'implemented',
82
+ note: 'Data-flow only. Covers file_get_contents, fopen, readfile, file, file_put_contents, unlink, opendir, scandir, copy, rename, mkdir and rmdir. NOT covered: include / require, which are statements, not calls, and are not in the sink tables.',
83
+ },
84
+ go: {
85
+ status: 'implemented',
86
+ note: 'Data-flow only. Covers os and ioutil Open / OpenFile / ReadFile / WriteFile / Create / Remove / RemoveAll / ReadDir / Mkdir / Rename and http.ServeFile. NOT covered: http.FileServer, which is configured once rather than called with a path.',
87
+ },
88
+ c: {
89
+ status: 'implemented',
90
+ note: 'Data-flow only. Covers fopen, freopen, open, openat, creat, remove, unlink, rename and opendir. argv, getenv and stdin are reported low, to review: they are chosen by whoever runs the program.',
91
+ },
92
+ cpp: {
93
+ status: 'partial',
94
+ note: 'PARTIAL: the C library calls only. std::ifstream / std::ofstream and std::filesystem are NOT modelled - a stream is opened by a declaration, not a call.',
95
+ },
96
+ },
97
+ /* Deliberately silent: see the header. The taint engine emits every finding for this rule id. */
98
+ check(): RuleHit | null {
99
+ return null;
100
+ },
101
+ };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * THE RULE REGISTRY
3
+ *
4
+ * One list, one place. Adding a rule = write the file, import it, add it here.
5
+ * The engine imports nothing but this.
6
+ *
7
+ * `validateRegistry()` runs on every scan. It refuses to start if a rule breaks
8
+ * the honesty contract - for example by declaring a language as `implemented`
9
+ * with an empty note. A rule that cannot say what it does not cover has not
10
+ * finished thinking, and shipping it silently is the failure mode this whole
11
+ * project is a reaction to.
12
+ */
13
+
14
+ import type { LanguageId } from '../parse/languages.js';
15
+ import { LANGUAGES } from '../parse/languages.js';
16
+ import type { Rule } from './contract.js';
17
+ import { codeInjectionRule } from './code-injection.js';
18
+ import { commandInjectionRule } from './command-injection.js';
19
+ import { hardcodedSecretRule } from './hardcoded-secret.js';
20
+ import { sqlInjectionRule } from './sql-injection.js';
21
+ import { ssrfRule } from './ssrf.js';
22
+ import { pathTraversalRule } from './path-traversal.js';
23
+ import { unsafeDeserializationRule } from './unsafe-deserialization.js';
24
+ import { weakHashRule } from './weak-hash.js';
25
+ import { formatStringRule } from './format-string.js';
26
+ import { unboundedCopyRule } from './unbounded-copy.js';
27
+ import { xssRule } from './xss.js';
28
+
29
+ export const ALL_RULES: readonly Rule[] = [
30
+ sqlInjectionRule,
31
+ commandInjectionRule,
32
+ formatStringRule,
33
+ unboundedCopyRule,
34
+ codeInjectionRule,
35
+ xssRule,
36
+ ssrfRule,
37
+ pathTraversalRule,
38
+ unsafeDeserializationRule,
39
+ hardcodedSecretRule,
40
+ weakHashRule,
41
+ ];
42
+
43
+ export function rulesForLanguage(language: LanguageId): Rule[] {
44
+ return ALL_RULES.filter((rule) => {
45
+ const support = rule.support[language];
46
+ return support !== undefined && support.status !== 'not-implemented';
47
+ });
48
+ }
49
+
50
+ export function getRule(id: string): Rule | undefined {
51
+ return ALL_RULES.find((rule) => rule.id === id);
52
+ }
53
+
54
+ /** Throws if the registry violates the project's own honesty requirements. */
55
+ export function validateRegistry(): void {
56
+ const problems: string[] = [];
57
+ const seen = new Set<string>();
58
+
59
+ for (const rule of ALL_RULES) {
60
+ if (seen.has(rule.id)) problems.push(`duplicate rule id: ${rule.id}`);
61
+ seen.add(rule.id);
62
+
63
+ if (rule.limitations.trim().length < 20) {
64
+ problems.push(`${rule.id}: 'limitations' is missing or too short to be useful`);
65
+ }
66
+ if (rule.explanation.trim().length < 40) {
67
+ problems.push(`${rule.id}: 'explanation' is missing or too short to teach anything`);
68
+ }
69
+ for (const language of LANGUAGES) {
70
+ const support = rule.support[language.id];
71
+ if (!support) continue;
72
+ if (support.note.trim().length < 15) {
73
+ problems.push(
74
+ `${rule.id}/${language.id}: support note is empty - a gap must be described, not implied`,
75
+ );
76
+ }
77
+ if (support.status !== 'implemented' && !/PARTIAL|NOT|not covered|missing/i.test(support.note)) {
78
+ problems.push(
79
+ `${rule.id}/${language.id}: status is '${support.status}' but the note does not say what is missing`,
80
+ );
81
+ }
82
+ }
83
+ }
84
+
85
+ if (problems.length > 0) {
86
+ throw new Error(
87
+ `Rule registry violates the honesty contract:\n - ${problems.join('\n - ')}`,
88
+ );
89
+ }
90
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * SQL INJECTION (CWE-89, OWASP A05:2025 - Injection)
3
+ *
4
+ * WHAT THE BUG IS, in plain language:
5
+ * A database doesn't receive "a question" - it receives a string of text that it
6
+ * reads as INSTRUCTIONS. If you build that string by gluing user input into it,
7
+ * the user gets to write part of your instructions.
8
+ *
9
+ * db.query("SELECT * FROM users WHERE id = " + userId)
10
+ *
11
+ * If someone sends `userId = 1 OR 1=1`, the database is asked for every user.
12
+ * If they send `1; DROP TABLE users --`, it may be asked to delete the table.
13
+ * The database is not tricked or hacked; it does exactly what the text says.
14
+ * The mistake happened when data and instructions were mixed into one string.
15
+ *
16
+ * THE FIX, so the rule can suggest it:
17
+ * Parameterised queries (also called prepared statements). You send the query
18
+ * shape and the values SEPARATELY:
19
+ *
20
+ * db.query("SELECT * FROM users WHERE id = ?", [userId])
21
+ *
22
+ * The driver never lets the value become part of the instruction, whatever it
23
+ * contains. This is not "escaping harder" - it is a structurally different
24
+ * thing, and it is why "we sanitise our inputs" is a weaker answer than
25
+ * "we parameterise our queries".
26
+ *
27
+ * WHAT THIS RULE ACTUALLY CHECKS (and does not):
28
+ * We look for a call to a known query-executing method whose argument was
29
+ * BUILT rather than written, and whose visible text looks like SQL. We do NOT
30
+ * know whether the spliced-in value came from an attacker. That is data-flow
31
+ * analysis. The taint engine does exactly that, and when it succeeds the
32
+ * verified finding supersedes this one. Everything the tracer could not confirm
33
+ * stays here, signature-based, saying so on the finding itself.
34
+ */
35
+
36
+ import type { LanguageId } from '../parse/languages.js';
37
+ import { asAssignment, asCall, type Rule, type RuleHit } from './contract.js';
38
+ import {
39
+ analyzeStringExpression,
40
+ isStringBuildingExpression,
41
+ looksLikeSql,
42
+ expandsOnlyPlaceholders,
43
+ constantProof,
44
+ } from './lib/strings.js';
45
+ import { partsAreGuarded } from './lib/guards.js';
46
+
47
+ /**
48
+ * Methods that hand a string to a database engine.
49
+ * Case matters: Go and Java capitalise, JavaScript and Python do not.
50
+ */
51
+ const QUERY_METHODS: Record<LanguageId, readonly string[]> = {
52
+ javascript: [
53
+ 'query', 'execute', 'exec', 'raw', 'prepare', 'run', 'all', 'get', 'each',
54
+ 'queryRaw', '$queryRawUnsafe', '$executeRawUnsafe', 'unsafe', 'sequelize.query',
55
+ ],
56
+ typescript: [
57
+ 'query', 'execute', 'exec', 'raw', 'prepare', 'run', 'all', 'get', 'each',
58
+ 'queryRaw', '$queryRawUnsafe', '$executeRawUnsafe', 'unsafe',
59
+ ],
60
+ python: [
61
+ 'execute', 'executemany', 'executescript', 'raw', 'text', 'query', 'from_statement',
62
+ ],
63
+ java: [
64
+ 'executeQuery', 'executeUpdate', 'execute', 'prepareStatement', 'prepareCall',
65
+ 'createQuery', 'createNativeQuery', 'createSQLQuery', 'rawQuery', 'addBatch',
66
+ 'queryForObject', 'queryForList', 'update',
67
+ ],
68
+ go: [
69
+ 'Query', 'QueryRow', 'QueryContext', 'QueryRowContext', 'Exec', 'ExecContext',
70
+ 'Prepare', 'PrepareContext', 'Raw', 'MustExec', 'Select', 'Get', 'NamedExec',
71
+ ],
72
+ // PHP mixes procedural functions (mysqli_query) with PDO/mysqli methods
73
+ // ($pdo->query). Both are bare names by the time the shape reaches us, so
74
+ // one list covers both. `prepare` is included on purpose: a prepared
75
+ // statement built by concatenation is not parameterised, it is just a slower
76
+ // injection.
77
+ php: [
78
+ 'query', 'exec', 'prepare', 'unprepared', 'statement', 'select', 'selectOne',
79
+ 'mysqli_query', 'mysqli_real_query', 'mysqli_multi_query', 'mysqli_prepare',
80
+ 'pg_query', 'pg_send_query', 'sqlite_query', 'sqlsrv_query',
81
+ 'mysql_query', 'db_query',
82
+ ],
83
+ /*
84
+ * C database APIs are plain functions, not methods, and every one of them
85
+ * takes the SQL as a bare char* - there is no query builder to be confused
86
+ * with. The parameterised forms have DIFFERENT names (mysql_stmt_prepare,
87
+ * PQexecParams, sqlite3_prepare_v2), so they are absent here and the correct
88
+ * code stays quiet, which is the same distinction the other languages draw
89
+ * between `query` and `prepare`.
90
+ */
91
+ c: [
92
+ 'mysql_query', 'mysql_real_query', 'PQexec', 'sqlite3_exec',
93
+ 'sqlite3_get_table', 'dbsqlexec', 'SQLExecDirect',
94
+ ],
95
+ // C++ adds the method-call forms that wrapper libraries expose.
96
+ cpp: [
97
+ 'mysql_query', 'mysql_real_query', 'PQexec', 'sqlite3_exec',
98
+ 'sqlite3_get_table', 'SQLExecDirect',
99
+ 'query', 'execute', 'exec',
100
+ ],
101
+ };
102
+
103
+ const METHOD_SETS: Record<LanguageId, ReadonlySet<string>> = {
104
+ javascript: new Set(QUERY_METHODS.javascript),
105
+ typescript: new Set(QUERY_METHODS.typescript),
106
+ python: new Set(QUERY_METHODS.python),
107
+ java: new Set(QUERY_METHODS.java),
108
+ go: new Set(QUERY_METHODS.go),
109
+ php: new Set(QUERY_METHODS.php),
110
+ c: new Set(QUERY_METHODS.c),
111
+ cpp: new Set(QUERY_METHODS.cpp),
112
+ };
113
+
114
+ const MECHANISM_WORDS: Record<string, string> = {
115
+ concatenation: 'string concatenation (+)',
116
+ interpolation: 'string interpolation (template literal / f-string)',
117
+ 'format-call': 'a string-formatting call (Sprintf / .format / String.format)',
118
+ opaque: 'a variable whose contents we cannot see here',
119
+ };
120
+
121
+ export const sqlInjectionRule: Rule = {
122
+ id: 'sql-injection',
123
+ name: 'SQL query assembled from a built string',
124
+ cwe: 'CWE-89',
125
+ owasp: 'A05:2025 Injection',
126
+ severity: 'critical',
127
+ summary: 'The SQL query text is built from pieces; if any piece comes from outside, it can rewrite what the query does.',
128
+ fix:
129
+ 'Use a parameterised query: keep the SQL fixed with placeholders and pass values separately, e.g. cursor.execute("... WHERE name = ?", (name,)).',
130
+ explanation:
131
+ 'A database reads the string you give it as instructions, not as a question. ' +
132
+ 'When a query is glued together from pieces, anything spliced in can change what ' +
133
+ 'the query does - reading other users\' rows, bypassing a login check, or deleting ' +
134
+ 'a table. The fix is parameterised queries: send the query and the values ' +
135
+ 'separately, so a value can never be read as an instruction.',
136
+ limitations:
137
+ 'UNVERIFIED (signature-based): we confirmed the query string was built rather than ' +
138
+ 'written literally. We did NOT confirm that any part of it is attacker-controlled. ' +
139
+ 'If every spliced value is a hard-coded constant or an internal enum, this is safe ' +
140
+ 'and is a false positive. When the data-flow engine CAN confirm the source, the ' +
141
+ 'finding is upgraded to flow-verified and this text is replaced by a traced path. ' +
142
+ 'When every spliced value is PROVABLY a fixed literal - including a write in a ' +
143
+ 'branch that cannot run, a condition decided at compile time, literals joined ' +
144
+ 'with `+`, a Java helper reached through `new X().m()` or through a local that ' +
145
+ 'certainly holds a `new X()` (declared once, never written again, used only as the ' +
146
+ 'receiver of calls), a private/static method, ' +
147
+ 'or a static method named through its class (`Utils.m()`, in this file or another, ' +
148
+ 'when exactly one class of that full name and one method of that name exist in ' +
149
+ 'the scan) whose returns are built from literals and parameters passed fixed ' +
150
+ 'values, a read from a local Java map or list that returns a literal - or a local ' +
151
+ 'name fixed where it was stored - put under that key or at that position, or a ' +
152
+ 'dirty write that is certainly overwritten ' +
153
+ 'before the query - the guess is withdrawn and listed as proved clean with the ' +
154
+ 'reason, rather than reported. That proof stops at one helper level, does not ' +
155
+ 'follow helpers outside Java or choose between overloads, does not use the ' +
156
+ 'last-write rule in C or C++, and does not model String methods or any collection ' +
157
+ 'other than those local maps and lists, so a value fixed by any of those is still ' +
158
+ 'reported here. A name must be declared where the read can see it (Java, JavaScript, ' +
159
+ 'TypeScript, Go block scoping); one declared outside the reading function counts only ' +
160
+ 'when no other function in its file writes it, and a Java field only when it is ' +
161
+ 'private or final; inside a helper, a field written from a parameter is input, ' +
162
+ 'since it keeps what an EARLIER call passed. What the proof does NOT see, and so ' +
163
+ 'can wrongly call fixed: a ' +
164
+ 'write from ANOTHER file to a JavaScript or Python module-level name or a Go package ' +
165
+ 'variable declared in this file; a member inherited from a superclass outside the ' +
166
+ 'scan (a library class such as HttpServlet), which could shadow a name or supply an ' +
167
+ 'overload; a write made by reflection (`Field.set` with setAccessible, from code ' +
168
+ 'that never names the field); and a write made through `new Function(...)` or ' +
169
+ 'a string handed to setTimeout. A withdrawal whose values are all plain literals ' +
170
+ 'leaves no proved-clean record. Three rounds of an agent trying to break this proof ' +
171
+ 'found 70 ways in; every one is fixed and kept as a test in tests/probes/, and a ' +
172
+ 'fourth round would likely find more.',
173
+ shapes: ['call', 'assignment'],
174
+ support: {
175
+ javascript: {
176
+ status: 'implemented',
177
+ note: 'Covers node-postgres, mysql/mysql2, sqlite3, knex .raw, Prisma $queryRawUnsafe, Sequelize .query.',
178
+ },
179
+ typescript: {
180
+ status: 'implemented',
181
+ note: 'Same coverage as JavaScript; type annotations are ignored because a `string` type says nothing about trust.',
182
+ },
183
+ python: {
184
+ status: 'implemented',
185
+ note: 'Covers DB-API cursor.execute/executemany, SQLAlchemy text()/raw(). ORM query-builder misuse is not modelled.',
186
+ },
187
+ java: {
188
+ status: 'implemented',
189
+ note: 'Covers JDBC Statement/PreparedStatement, JPA createQuery/createNativeQuery, Spring JdbcTemplate. StringBuilder.append chains that build SQL over several statements are NOT followed: the tracer tracks variables, not the interior of a mutable object.',
190
+ },
191
+ go: {
192
+ status: 'implemented',
193
+ note: 'Covers database/sql and sqlx. Go code frequently builds SQL with fmt.Sprintf even when every value is a constant, so the signature pass is noisier here than elsewhere - the data-flow engine is what separates the real ones.',
194
+ },
195
+ php: {
196
+ status: 'implemented',
197
+ note: 'Covers PDO and mysqli in both their forms - the procedural functions (mysqli_query, pg_query) and the object methods ($pdo->query, ->prepare). `prepare` is watched on purpose: a prepared statement whose SQL was concatenated is not parameterised, it is a slower injection. NOT covered: an ORM that builds SQL inside its own package, and .blade.php / .twig templates, which are not parsed.',
198
+ },
199
+ c: {
200
+ status: 'implemented',
201
+ note:
202
+ 'Covers the C database APIs, which are plain functions taking the statement as a bare char*: mysql_query, mysql_real_query, PQexec, sqlite3_exec, sqlite3_get_table and SQLExecDirect. The parameterised forms have DIFFERENT names (mysql_stmt_prepare, PQexecParams, sqlite3_prepare_v2) so correct code stays quiet. Queries are almost always built by sprintf into a char buffer, which the out-parameter tracking follows.',
203
+ },
204
+ cpp: {
205
+ status: 'implemented',
206
+ note:
207
+ '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.',
208
+ },
209
+ },
210
+ check(shape, ctx): RuleHit | null {
211
+ const language = ctx.language;
212
+
213
+ /* ---- Case 1: the query text is visible right at the call site ---- */
214
+ const call = asCall(shape);
215
+ if (call) {
216
+ if (!METHOD_SETS[language].has(call.calleeName)) return null;
217
+
218
+ for (const arg of call.args) {
219
+ // Only look at arguments that could BE a string. An options object
220
+ // `{ text: "...", values: [...] }` is a container, not a query.
221
+ if (!isStringBuildingExpression(arg, language)) continue;
222
+ const built = analyzeStringExpression(arg, language);
223
+ // Two conditions, both required:
224
+ // 1. the string was assembled, not written whole, and
225
+ // 2. what we CAN read of it actually looks like SQL.
226
+ // Condition 2 is what stops us shouting about `log("hi " + name)`.
227
+ if (!built.isDynamic) continue;
228
+ if (!looksLikeSql(built.literalText)) continue;
229
+ // "Assembled" from values that are all fixed literals is not assembled
230
+ // in any way a reader cares about. When saying so took reasoning - a
231
+ // branch that cannot run, a condition fixed at compile time - the
232
+ // withdrawal leaves a receipt, because reasoning can be wrong.
233
+ const argProof = constantProof(arg, built.dynamicParts, language, { staticMethod: ctx.staticMethod, javaTypeInScan: ctx.javaTypeInScan });
234
+ if (argProof) {
235
+ if (argProof.reasoned) ctx.noteClean?.('sql-injection', arg, argProof.reason);
236
+ continue;
237
+ }
238
+ /*
239
+ * A VALIDATION GUARD. `is_numeric($octet[0])` transforms nothing, so the
240
+ * sanitiser model never saw it - but inside the branch it guards, the
241
+ * value provably holds no metacharacter. This is DVWA's own fix for
242
+ * command injection, and reporting it was the seventh time this project
243
+ * punished a corrected file. See lib/guards.ts for how narrow the list is.
244
+ */
245
+ if (partsAreGuarded(arg, built.dynamicParts, language, 'sql')) continue;
246
+ // `${new Array(n).fill('(?,?)').join(',')}` expands to placeholders,
247
+ // never to data. Reporting it punishes the correct bulk-insert form.
248
+ if (built.dynamicParts.every((part) => expandsOnlyPlaceholders(part))) continue;
249
+
250
+ const how = MECHANISM_WORDS[built.mechanism] ?? built.mechanism;
251
+ return {
252
+ node: arg,
253
+ message: `SQL passed to ${call.calleeText}() is built with ${built.mechanism}`,
254
+ reasoning:
255
+ `\`${call.calleeText}()\` is a database query method. Its argument is not a ` +
256
+ `fixed string - it was assembled using ${how}. The literal part reads like SQL ` +
257
+ `("${built.literalText.replace(/\s+/g, ' ').trim().slice(0, 70)}"), and the ` +
258
+ `spliced-in part${built.dynamicParts.length === 1 ? ' is' : 's are'} ` +
259
+ `${built.dynamicParts.slice(0, 3).map((p) => `\`${p}\``).join(', ')}. ` +
260
+ `If any of those can be influenced by a user, they can rewrite the query.`,
261
+ };
262
+ }
263
+ return null;
264
+ }
265
+
266
+ /* ---- Case 2: SQL is built into a variable first ----
267
+ * `const sql = "SELECT ... " + id;` then later `db.query(sql)`.
268
+ * The SIGNATURE pass cannot connect those two statements - that is data-flow
269
+ * analysis, and the taint engine does it now. This branch stays as the
270
+ * fallback for everything the tracer could not confirm: no recognised
271
+ * source, a value arriving from another file, a framework we do not model.
272
+ * When the tracer DOES confirm it, the verified finding supersedes this one
273
+ * automatically, so the two never both appear. */
274
+ const assignment = asAssignment(shape);
275
+ if (assignment) {
276
+ // The scope gate. Without it, assigning a large object literal to a
277
+ // variable let every string inside it merge into one fake "query".
278
+ // This is the check that stopped Defuse reporting its own rule files.
279
+ if (!isStringBuildingExpression(assignment.value, language)) return null;
280
+ const built = analyzeStringExpression(assignment.value, language);
281
+ if (!built.isDynamic) return null;
282
+ if (!looksLikeSql(built.literalText)) return null;
283
+ const buildProof = constantProof(assignment.value, built.dynamicParts, language, { staticMethod: ctx.staticMethod, javaTypeInScan: ctx.javaTypeInScan });
284
+ if (buildProof) {
285
+ if (buildProof.reasoned) ctx.noteClean?.('sql-injection', assignment.node, buildProof.reason);
286
+ return null;
287
+ }
288
+ if (partsAreGuarded(assignment.value, built.dynamicParts, language, 'sql')) return null;
289
+ if (built.dynamicParts.every((part) => expandsOnlyPlaceholders(part))) return null;
290
+
291
+ return {
292
+ node: assignment.node,
293
+ severity: 'high',
294
+ message: `SQL string \`${assignment.targetName}\` is built with ${built.mechanism}`,
295
+ reasoning:
296
+ `\`${assignment.targetName}\` is assigned a string that reads like SQL and was ` +
297
+ `assembled using ${MECHANISM_WORDS[built.mechanism] ?? built.mechanism}. ` +
298
+ `The tracer did not confirm that this variable reaches a database call, so ` +
299
+ `this is reported as dangerous construction, not as a confirmed injection.`,
300
+ limitations:
301
+ 'UNVERIFIED (signature-based): we saw SQL text being assembled. We did NOT verify ' +
302
+ 'that it reaches a query method, and we did NOT verify that any part is ' +
303
+ 'attacker-controlled. The data-flow engine ran and could not confirm either step ' +
304
+ 'here - most often because no recognised source feeds it, or the value comes from ' +
305
+ 'another file.',
306
+ };
307
+ }
308
+
309
+ return null;
310
+ },
311
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * SERVER-SIDE REQUEST FORGERY - SSRF (CWE-918, OWASP A01:2025 - Broken Access Control)
3
+ *
4
+ * WHAT THE BUG IS, in plain language:
5
+ * Your server fetches a URL that the user chose. That sounds harmless - it is
6
+ * just an HTTP request - until you remember WHERE your server is standing.
7
+ *
8
+ * const data = await fetch(req.query.target);
9
+ *
10
+ * The attacker's browser cannot reach your database, your admin panel on
11
+ * port 8080, or `169.254.169.254` - the cloud metadata endpoint that hands out
12
+ * your instance's credentials to anything inside the network. Your server can
13
+ * reach all three. So the attacker stops trying to get in, and asks your server
14
+ * to go and fetch it for them. The firewall sees a request from a trusted
15
+ * machine and lets it through.
16
+ *
17
+ * THE FIX is an allowlist of hosts, not a blocklist. Blocklists lose, always:
18
+ * `localhost` has `127.0.0.1`, `127.1`, `0.0.0.0`, `[::1]`, `2130706433`, a
19
+ * DNS name that resolves to loopback, and a redirect from a public host to a
20
+ * private one. Decide the small set of hosts you MEANT to talk to, resolve the
21
+ * URL, and check membership - then fetch.
22
+ *
23
+ * ============================================================================
24
+ * WHY THIS RULE HAS NO SIGNATURE PASS
25
+ * ============================================================================
26
+ * Every one of the other rules can say something useful from shape alone: a
27
+ * query built by concatenation is suspicious whatever flows into it. This one
28
+ * cannot. "This code makes an HTTP request" is not a finding - it is what
29
+ * servers do all day, and a signature pass here would report every API client,
30
+ * every webhook, every health check in the codebase.
31
+ *
32
+ * The finding only exists when the URL is attacker-chosen, and that is a
33
+ * data-flow question, not a pattern. So `check()` deliberately returns null
34
+ * always, and every SSRF finding this tool produces is flow-verified with a
35
+ * printed path. It is the first rule where "we cannot say anything without the
36
+ * tracer" is the honest answer rather than a limitation to apologise for.
37
+ */
38
+
39
+ import { type Rule, type RuleHit } from './contract.js';
40
+
41
+ export const ssrfRule: Rule = {
42
+ id: 'ssrf',
43
+ name: 'Server fetches a URL the user chose',
44
+ cwe: 'CWE-918',
45
+ owasp: 'A01:2025 Broken Access Control',
46
+ severity: 'high',
47
+ summary: 'The server fetches a URL the request may choose, so an attacker could make it reach internal systems.',
48
+ fix:
49
+ 'Only fetch hosts on an allowlist, checked after the URL is resolved, and do not follow redirects off it.',
50
+ explanation:
51
+ 'Your server sits inside the network. It can reach the database, the admin ' +
52
+ 'panel, and the cloud metadata endpoint that hands out instance credentials - ' +
53
+ 'and the attacker\'s browser cannot. When the URL your server fetches is chosen ' +
54
+ 'by a request, the attacker stops trying to get past the firewall and simply ' +
55
+ 'asks your server to fetch the thing for them. The fix is an allowlist of hosts ' +
56
+ 'you meant to talk to, checked after resolving the URL. A blocklist loses: ' +
57
+ 'localhost is also 127.1, 0.0.0.0, [::1], 2130706433, a DNS name pointing at ' +
58
+ 'loopback, and a redirect from a public host to a private one.',
59
+ limitations:
60
+ 'FLOW-VERIFIED ONLY: this rule has no pattern pass, because "makes an HTTP ' +
61
+ 'request" describes almost every server and would report every API client in ' +
62
+ 'the codebase. A finding here always comes from a traced path from a request ' +
63
+ 'value to the fetch. Only one that can choose the DESTINATION is proven: a value ' +
64
+ 'spliced in after a fixed start of the address (`https://host/path/`, `${base}/`, ' +
65
+ '`./`), or one read from a browser-only source (the address bar, browser storage), ' +
66
+ 'cannot send the request to a server the attacker picks, and is reported as to ' +
67
+ 'review with that reason instead. DELIBERATE GAP: we cannot see a host allowlist. Code that ' +
68
+ 'correctly resolves the URL and checks the hostname against a fixed set is ' +
69
+ 'still reported, because recognising a correct allowlist means understanding ' +
70
+ 'what the check compares and what it does on failure - which is beyond what ' +
71
+ 'this engine models. Treat an SSRF finding as "confirm the host check", not as ' +
72
+ '"this is definitely exploitable". A REDIRECT from an allowed host to an ' +
73
+ 'internal one is invisible to any static tool, allowlist or not.',
74
+ shapes: ['call'],
75
+ support: {
76
+ javascript: {
77
+ status: 'implemented',
78
+ note: 'Data-flow only. Covers http/https get and request, fetch, axios, got, node-fetch and request. NOT covered: a URL assembled inside an options object (fetch({url: dirty})) - we read the URL argument positionally.',
79
+ },
80
+ typescript: {
81
+ status: 'implemented',
82
+ note: 'Data-flow only. Identical to JavaScript.',
83
+ },
84
+ python: {
85
+ status: 'implemented',
86
+ note: 'Data-flow only. Covers requests (get/post/put/delete/head/request), urllib.request.urlopen and httpx. NOT covered: aiohttp sessions, where the URL is passed to a method on a session object we do not model.',
87
+ },
88
+ java: {
89
+ status: 'partial',
90
+ note: 'PARTIAL, data-flow only. Covers new URL(x), URI.create(x), HttpRequest.newBuilder(x), and RestTemplate getForObject/getForEntity/postForObject/exchange on a receiver named or typed RestTemplate (or `rest`). The URL or URI being built is the sink, so new URL(x) used only to read its host is reported too. NOT covered: new URI(x) on its own, HttpRequest.newBuilder().uri(x), WebClient, OkHttp, Apache HttpClient, and a URL held in an object field.',
91
+ },
92
+ php: {
93
+ status: 'implemented',
94
+ note: 'Data-flow only. Covers file_get_contents, fopen, curl_setopt with CURLOPT_URL, and curl_init with a URL argument. Note that file_get_contents doubles as a file reader, so a finding here may be path traversal wearing a URL\'s clothes - both are worth the same second look.',
95
+ },
96
+ go: {
97
+ status: 'partial',
98
+ note: 'PARTIAL, data-flow only. Covers the net/http package functions http.Get, http.Post, http.Head, http.PostForm, http.NewRequest and http.NewRequestWithContext. NOT covered: the same methods on a client (client.Get(u), http.DefaultClient.Get(u)), because the receiver is not `http`, and third-party clients such as resty.',
99
+ },
100
+ c: {
101
+ status: 'not-implemented',
102
+ note:
103
+ 'libcurl is how C makes outbound requests - curl_easy_setopt(handle, CURLOPT_URL, url) - and it is NOT modelled: the URL is one argument of a variadic setter whose meaning depends on the option constant next to it, which this engine does not read. Server-side request forgery written with libcurl is missed entirely.',
104
+ },
105
+ cpp: {
106
+ status: 'not-implemented',
107
+ note:
108
+ 'Not implemented. C++ code reaches the network through libcurl (not modelled - the URL is one argument of a variadic setter), or through a framework HTTP client such as cpp-httplib or Boost.Beast, none of which are in the sink tables.',
109
+ },
110
+ },
111
+ /*
112
+ * Deliberately silent. See the header: there is no honest pattern for this
113
+ * bug, only a flow. The taint engine emits every finding for this rule id.
114
+ */
115
+ check(): RuleHit | null {
116
+ return null;
117
+ },
118
+ };