@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,299 @@
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
+ import { asAssignment, asCall } from './contract.js';
36
+ import { analyzeStringExpression, isStringBuildingExpression, looksLikeSql, expandsOnlyPlaceholders, constantProof, } from './lib/strings.js';
37
+ import { partsAreGuarded } from './lib/guards.js';
38
+ /**
39
+ * Methods that hand a string to a database engine.
40
+ * Case matters: Go and Java capitalise, JavaScript and Python do not.
41
+ */
42
+ const QUERY_METHODS = {
43
+ javascript: [
44
+ 'query', 'execute', 'exec', 'raw', 'prepare', 'run', 'all', 'get', 'each',
45
+ 'queryRaw', '$queryRawUnsafe', '$executeRawUnsafe', 'unsafe', 'sequelize.query',
46
+ ],
47
+ typescript: [
48
+ 'query', 'execute', 'exec', 'raw', 'prepare', 'run', 'all', 'get', 'each',
49
+ 'queryRaw', '$queryRawUnsafe', '$executeRawUnsafe', 'unsafe',
50
+ ],
51
+ python: [
52
+ 'execute', 'executemany', 'executescript', 'raw', 'text', 'query', 'from_statement',
53
+ ],
54
+ java: [
55
+ 'executeQuery', 'executeUpdate', 'execute', 'prepareStatement', 'prepareCall',
56
+ 'createQuery', 'createNativeQuery', 'createSQLQuery', 'rawQuery', 'addBatch',
57
+ 'queryForObject', 'queryForList', 'update',
58
+ ],
59
+ go: [
60
+ 'Query', 'QueryRow', 'QueryContext', 'QueryRowContext', 'Exec', 'ExecContext',
61
+ 'Prepare', 'PrepareContext', 'Raw', 'MustExec', 'Select', 'Get', 'NamedExec',
62
+ ],
63
+ // PHP mixes procedural functions (mysqli_query) with PDO/mysqli methods
64
+ // ($pdo->query). Both are bare names by the time the shape reaches us, so
65
+ // one list covers both. `prepare` is included on purpose: a prepared
66
+ // statement built by concatenation is not parameterised, it is just a slower
67
+ // injection.
68
+ php: [
69
+ 'query', 'exec', 'prepare', 'unprepared', 'statement', 'select', 'selectOne',
70
+ 'mysqli_query', 'mysqli_real_query', 'mysqli_multi_query', 'mysqli_prepare',
71
+ 'pg_query', 'pg_send_query', 'sqlite_query', 'sqlsrv_query',
72
+ 'mysql_query', 'db_query',
73
+ ],
74
+ /*
75
+ * C database APIs are plain functions, not methods, and every one of them
76
+ * takes the SQL as a bare char* - there is no query builder to be confused
77
+ * with. The parameterised forms have DIFFERENT names (mysql_stmt_prepare,
78
+ * PQexecParams, sqlite3_prepare_v2), so they are absent here and the correct
79
+ * code stays quiet, which is the same distinction the other languages draw
80
+ * between `query` and `prepare`.
81
+ */
82
+ c: [
83
+ 'mysql_query', 'mysql_real_query', 'PQexec', 'sqlite3_exec',
84
+ 'sqlite3_get_table', 'dbsqlexec', 'SQLExecDirect',
85
+ ],
86
+ // C++ adds the method-call forms that wrapper libraries expose.
87
+ cpp: [
88
+ 'mysql_query', 'mysql_real_query', 'PQexec', 'sqlite3_exec',
89
+ 'sqlite3_get_table', 'SQLExecDirect',
90
+ 'query', 'execute', 'exec',
91
+ ],
92
+ };
93
+ const METHOD_SETS = {
94
+ javascript: new Set(QUERY_METHODS.javascript),
95
+ typescript: new Set(QUERY_METHODS.typescript),
96
+ python: new Set(QUERY_METHODS.python),
97
+ java: new Set(QUERY_METHODS.java),
98
+ go: new Set(QUERY_METHODS.go),
99
+ php: new Set(QUERY_METHODS.php),
100
+ c: new Set(QUERY_METHODS.c),
101
+ cpp: new Set(QUERY_METHODS.cpp),
102
+ };
103
+ const MECHANISM_WORDS = {
104
+ concatenation: 'string concatenation (+)',
105
+ interpolation: 'string interpolation (template literal / f-string)',
106
+ 'format-call': 'a string-formatting call (Sprintf / .format / String.format)',
107
+ opaque: 'a variable whose contents we cannot see here',
108
+ };
109
+ export const sqlInjectionRule = {
110
+ id: 'sql-injection',
111
+ name: 'SQL query assembled from a built string',
112
+ cwe: 'CWE-89',
113
+ owasp: 'A05:2025 Injection',
114
+ severity: 'critical',
115
+ summary: 'The SQL query text is built from pieces; if any piece comes from outside, it can rewrite what the query does.',
116
+ fix: 'Use a parameterised query: keep the SQL fixed with placeholders and pass values separately, e.g. cursor.execute("... WHERE name = ?", (name,)).',
117
+ explanation: 'A database reads the string you give it as instructions, not as a question. ' +
118
+ 'When a query is glued together from pieces, anything spliced in can change what ' +
119
+ 'the query does - reading other users\' rows, bypassing a login check, or deleting ' +
120
+ 'a table. The fix is parameterised queries: send the query and the values ' +
121
+ 'separately, so a value can never be read as an instruction.',
122
+ limitations: 'UNVERIFIED (signature-based): we confirmed the query string was built rather than ' +
123
+ 'written literally. We did NOT confirm that any part of it is attacker-controlled. ' +
124
+ 'If every spliced value is a hard-coded constant or an internal enum, this is safe ' +
125
+ 'and is a false positive. When the data-flow engine CAN confirm the source, the ' +
126
+ 'finding is upgraded to flow-verified and this text is replaced by a traced path. ' +
127
+ 'When every spliced value is PROVABLY a fixed literal - including a write in a ' +
128
+ 'branch that cannot run, a condition decided at compile time, literals joined ' +
129
+ 'with `+`, a Java helper reached through `new X().m()` or through a local that ' +
130
+ 'certainly holds a `new X()` (declared once, never written again, used only as the ' +
131
+ 'receiver of calls), a private/static method, ' +
132
+ 'or a static method named through its class (`Utils.m()`, in this file or another, ' +
133
+ 'when exactly one class of that full name and one method of that name exist in ' +
134
+ 'the scan) whose returns are built from literals and parameters passed fixed ' +
135
+ 'values, a read from a local Java map or list that returns a literal - or a local ' +
136
+ 'name fixed where it was stored - put under that key or at that position, or a ' +
137
+ 'dirty write that is certainly overwritten ' +
138
+ 'before the query - the guess is withdrawn and listed as proved clean with the ' +
139
+ 'reason, rather than reported. That proof stops at one helper level, does not ' +
140
+ 'follow helpers outside Java or choose between overloads, does not use the ' +
141
+ 'last-write rule in C or C++, and does not model String methods or any collection ' +
142
+ 'other than those local maps and lists, so a value fixed by any of those is still ' +
143
+ 'reported here. A name must be declared where the read can see it (Java, JavaScript, ' +
144
+ 'TypeScript, Go block scoping); one declared outside the reading function counts only ' +
145
+ 'when no other function in its file writes it, and a Java field only when it is ' +
146
+ 'private or final; inside a helper, a field written from a parameter is input, ' +
147
+ 'since it keeps what an EARLIER call passed. What the proof does NOT see, and so ' +
148
+ 'can wrongly call fixed: a ' +
149
+ 'write from ANOTHER file to a JavaScript or Python module-level name or a Go package ' +
150
+ 'variable declared in this file; a member inherited from a superclass outside the ' +
151
+ 'scan (a library class such as HttpServlet), which could shadow a name or supply an ' +
152
+ 'overload; a write made by reflection (`Field.set` with setAccessible, from code ' +
153
+ 'that never names the field); and a write made through `new Function(...)` or ' +
154
+ 'a string handed to setTimeout. A withdrawal whose values are all plain literals ' +
155
+ 'leaves no proved-clean record. Three rounds of an agent trying to break this proof ' +
156
+ 'found 70 ways in; every one is fixed and kept as a test in tests/probes/, and a ' +
157
+ 'fourth round would likely find more.',
158
+ shapes: ['call', 'assignment'],
159
+ support: {
160
+ javascript: {
161
+ status: 'implemented',
162
+ note: 'Covers node-postgres, mysql/mysql2, sqlite3, knex .raw, Prisma $queryRawUnsafe, Sequelize .query.',
163
+ },
164
+ typescript: {
165
+ status: 'implemented',
166
+ note: 'Same coverage as JavaScript; type annotations are ignored because a `string` type says nothing about trust.',
167
+ },
168
+ python: {
169
+ status: 'implemented',
170
+ note: 'Covers DB-API cursor.execute/executemany, SQLAlchemy text()/raw(). ORM query-builder misuse is not modelled.',
171
+ },
172
+ java: {
173
+ status: 'implemented',
174
+ 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.',
175
+ },
176
+ go: {
177
+ status: 'implemented',
178
+ 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.',
179
+ },
180
+ php: {
181
+ status: 'implemented',
182
+ 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.',
183
+ },
184
+ c: {
185
+ status: 'implemented',
186
+ note: '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.',
187
+ },
188
+ cpp: {
189
+ status: 'implemented',
190
+ 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.',
191
+ },
192
+ },
193
+ check(shape, ctx) {
194
+ const language = ctx.language;
195
+ /* ---- Case 1: the query text is visible right at the call site ---- */
196
+ const call = asCall(shape);
197
+ if (call) {
198
+ if (!METHOD_SETS[language].has(call.calleeName))
199
+ return null;
200
+ for (const arg of call.args) {
201
+ // Only look at arguments that could BE a string. An options object
202
+ // `{ text: "...", values: [...] }` is a container, not a query.
203
+ if (!isStringBuildingExpression(arg, language))
204
+ continue;
205
+ const built = analyzeStringExpression(arg, language);
206
+ // Two conditions, both required:
207
+ // 1. the string was assembled, not written whole, and
208
+ // 2. what we CAN read of it actually looks like SQL.
209
+ // Condition 2 is what stops us shouting about `log("hi " + name)`.
210
+ if (!built.isDynamic)
211
+ continue;
212
+ if (!looksLikeSql(built.literalText))
213
+ continue;
214
+ // "Assembled" from values that are all fixed literals is not assembled
215
+ // in any way a reader cares about. When saying so took reasoning - a
216
+ // branch that cannot run, a condition fixed at compile time - the
217
+ // withdrawal leaves a receipt, because reasoning can be wrong.
218
+ const argProof = constantProof(arg, built.dynamicParts, language, { staticMethod: ctx.staticMethod, javaTypeInScan: ctx.javaTypeInScan });
219
+ if (argProof) {
220
+ if (argProof.reasoned)
221
+ ctx.noteClean?.('sql-injection', arg, argProof.reason);
222
+ continue;
223
+ }
224
+ /*
225
+ * A VALIDATION GUARD. `is_numeric($octet[0])` transforms nothing, so the
226
+ * sanitiser model never saw it - but inside the branch it guards, the
227
+ * value provably holds no metacharacter. This is DVWA's own fix for
228
+ * command injection, and reporting it was the seventh time this project
229
+ * punished a corrected file. See lib/guards.ts for how narrow the list is.
230
+ */
231
+ if (partsAreGuarded(arg, built.dynamicParts, language, 'sql'))
232
+ continue;
233
+ // `${new Array(n).fill('(?,?)').join(',')}` expands to placeholders,
234
+ // never to data. Reporting it punishes the correct bulk-insert form.
235
+ if (built.dynamicParts.every((part) => expandsOnlyPlaceholders(part)))
236
+ continue;
237
+ const how = MECHANISM_WORDS[built.mechanism] ?? built.mechanism;
238
+ return {
239
+ node: arg,
240
+ message: `SQL passed to ${call.calleeText}() is built with ${built.mechanism}`,
241
+ reasoning: `\`${call.calleeText}()\` is a database query method. Its argument is not a ` +
242
+ `fixed string - it was assembled using ${how}. The literal part reads like SQL ` +
243
+ `("${built.literalText.replace(/\s+/g, ' ').trim().slice(0, 70)}"), and the ` +
244
+ `spliced-in part${built.dynamicParts.length === 1 ? ' is' : 's are'} ` +
245
+ `${built.dynamicParts.slice(0, 3).map((p) => `\`${p}\``).join(', ')}. ` +
246
+ `If any of those can be influenced by a user, they can rewrite the query.`,
247
+ };
248
+ }
249
+ return null;
250
+ }
251
+ /* ---- Case 2: SQL is built into a variable first ----
252
+ * `const sql = "SELECT ... " + id;` then later `db.query(sql)`.
253
+ * The SIGNATURE pass cannot connect those two statements - that is data-flow
254
+ * analysis, and the taint engine does it now. This branch stays as the
255
+ * fallback for everything the tracer could not confirm: no recognised
256
+ * source, a value arriving from another file, a framework we do not model.
257
+ * When the tracer DOES confirm it, the verified finding supersedes this one
258
+ * automatically, so the two never both appear. */
259
+ const assignment = asAssignment(shape);
260
+ if (assignment) {
261
+ // The scope gate. Without it, assigning a large object literal to a
262
+ // variable let every string inside it merge into one fake "query".
263
+ // This is the check that stopped Defuse reporting its own rule files.
264
+ if (!isStringBuildingExpression(assignment.value, language))
265
+ return null;
266
+ const built = analyzeStringExpression(assignment.value, language);
267
+ if (!built.isDynamic)
268
+ return null;
269
+ if (!looksLikeSql(built.literalText))
270
+ return null;
271
+ const buildProof = constantProof(assignment.value, built.dynamicParts, language, { staticMethod: ctx.staticMethod, javaTypeInScan: ctx.javaTypeInScan });
272
+ if (buildProof) {
273
+ if (buildProof.reasoned)
274
+ ctx.noteClean?.('sql-injection', assignment.node, buildProof.reason);
275
+ return null;
276
+ }
277
+ if (partsAreGuarded(assignment.value, built.dynamicParts, language, 'sql'))
278
+ return null;
279
+ if (built.dynamicParts.every((part) => expandsOnlyPlaceholders(part)))
280
+ return null;
281
+ return {
282
+ node: assignment.node,
283
+ severity: 'high',
284
+ message: `SQL string \`${assignment.targetName}\` is built with ${built.mechanism}`,
285
+ reasoning: `\`${assignment.targetName}\` is assigned a string that reads like SQL and was ` +
286
+ `assembled using ${MECHANISM_WORDS[built.mechanism] ?? built.mechanism}. ` +
287
+ `The tracer did not confirm that this variable reaches a database call, so ` +
288
+ `this is reported as dangerous construction, not as a confirmed injection.`,
289
+ limitations: 'UNVERIFIED (signature-based): we saw SQL text being assembled. We did NOT verify ' +
290
+ 'that it reaches a query method, and we did NOT verify that any part is ' +
291
+ 'attacker-controlled. The data-flow engine ran and could not confirm either step ' +
292
+ 'here - most often because no recognised source feeds it, or the value comes from ' +
293
+ 'another file.',
294
+ };
295
+ }
296
+ return null;
297
+ },
298
+ };
299
+ //# sourceMappingURL=sql-injection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sql-injection.js","sourceRoot":"","sources":["../../../src/rules/sql-injection.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
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
+ import { type Rule } from './contract.js';
39
+ export declare const ssrfRule: Rule;
@@ -0,0 +1,111 @@
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
+ export const ssrfRule = {
39
+ id: 'ssrf',
40
+ name: 'Server fetches a URL the user chose',
41
+ cwe: 'CWE-918',
42
+ owasp: 'A01:2025 Broken Access Control',
43
+ severity: 'high',
44
+ summary: 'The server fetches a URL the request may choose, so an attacker could make it reach internal systems.',
45
+ fix: 'Only fetch hosts on an allowlist, checked after the URL is resolved, and do not follow redirects off it.',
46
+ explanation: 'Your server sits inside the network. It can reach the database, the admin ' +
47
+ 'panel, and the cloud metadata endpoint that hands out instance credentials - ' +
48
+ 'and the attacker\'s browser cannot. When the URL your server fetches is chosen ' +
49
+ 'by a request, the attacker stops trying to get past the firewall and simply ' +
50
+ 'asks your server to fetch the thing for them. The fix is an allowlist of hosts ' +
51
+ 'you meant to talk to, checked after resolving the URL. A blocklist loses: ' +
52
+ 'localhost is also 127.1, 0.0.0.0, [::1], 2130706433, a DNS name pointing at ' +
53
+ 'loopback, and a redirect from a public host to a private one.',
54
+ limitations: 'FLOW-VERIFIED ONLY: this rule has no pattern pass, because "makes an HTTP ' +
55
+ 'request" describes almost every server and would report every API client in ' +
56
+ 'the codebase. A finding here always comes from a traced path from a request ' +
57
+ 'value to the fetch. Only one that can choose the DESTINATION is proven: a value ' +
58
+ 'spliced in after a fixed start of the address (`https://host/path/`, `${base}/`, ' +
59
+ '`./`), or one read from a browser-only source (the address bar, browser storage), ' +
60
+ 'cannot send the request to a server the attacker picks, and is reported as to ' +
61
+ 'review with that reason instead. DELIBERATE GAP: we cannot see a host allowlist. Code that ' +
62
+ 'correctly resolves the URL and checks the hostname against a fixed set is ' +
63
+ 'still reported, because recognising a correct allowlist means understanding ' +
64
+ 'what the check compares and what it does on failure - which is beyond what ' +
65
+ 'this engine models. Treat an SSRF finding as "confirm the host check", not as ' +
66
+ '"this is definitely exploitable". A REDIRECT from an allowed host to an ' +
67
+ 'internal one is invisible to any static tool, allowlist or not.',
68
+ shapes: ['call'],
69
+ support: {
70
+ javascript: {
71
+ status: 'implemented',
72
+ 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.',
73
+ },
74
+ typescript: {
75
+ status: 'implemented',
76
+ note: 'Data-flow only. Identical to JavaScript.',
77
+ },
78
+ python: {
79
+ status: 'implemented',
80
+ 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.',
81
+ },
82
+ java: {
83
+ status: 'partial',
84
+ 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.',
85
+ },
86
+ php: {
87
+ status: 'implemented',
88
+ 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.',
89
+ },
90
+ go: {
91
+ status: 'partial',
92
+ 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.',
93
+ },
94
+ c: {
95
+ status: 'not-implemented',
96
+ note: '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.',
97
+ },
98
+ cpp: {
99
+ status: 'not-implemented',
100
+ note: '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.',
101
+ },
102
+ },
103
+ /*
104
+ * Deliberately silent. See the header: there is no honest pattern for this
105
+ * bug, only a flow. The taint engine emits every finding for this rule id.
106
+ */
107
+ check() {
108
+ return null;
109
+ },
110
+ };
111
+ //# sourceMappingURL=ssrf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../../../src/rules/ssrf.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,MAAM,CAAC,MAAM,QAAQ,GAAS;IAC5B,EAAE,EAAE,MAAM;IACV,IAAI,EAAE,qCAAqC;IAC3C,GAAG,EAAE,SAAS;IACd,KAAK,EAAE,gCAAgC;IACvC,QAAQ,EAAE,MAAM;IAChB,OAAO,EAAE,uGAAuG;IAChH,GAAG,EACD,0GAA0G;IAC5G,WAAW,EACT,4EAA4E;QAC5E,+EAA+E;QAC/E,iFAAiF;QACjF,8EAA8E;QAC9E,iFAAiF;QACjF,4EAA4E;QAC5E,8EAA8E;QAC9E,+DAA+D;IACjE,WAAW,EACT,4EAA4E;QAC5E,8EAA8E;QAC9E,8EAA8E;QAC9E,kFAAkF;QAClF,mFAAmF;QACnF,oFAAoF;QACpF,gFAAgF;QAChF,6FAA6F;QAC7F,4EAA4E;QAC5E,8EAA8E;QAC9E,6EAA6E;QAC7E,gFAAgF;QAChF,0EAA0E;QAC1E,iEAAiE;IACnE,MAAM,EAAE,CAAC,MAAM,CAAC;IAChB,OAAO,EAAE;QACP,UAAU,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,oNAAoN;SAC3N;QACD,UAAU,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,0CAA0C;SACjD;QACD,MAAM,EAAE;YACN,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,+MAA+M;SACtN;QACD,IAAI,EAAE;YACJ,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,gcAAgc;SACvc;QACD,GAAG,EAAE;YACH,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,iRAAiR;SACxR;QACD,EAAE,EAAE;YACF,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,gUAAgU;SACvU;QACD,CAAC,EAAE;YACD,MAAM,EAAE,iBAAiB;YACzB,IAAI,EACF,6TAA6T;SAChU;QACD,GAAG,EAAE;YACH,MAAM,EAAE,iBAAiB;YACzB,IAAI,EACF,2OAA2O;SAC9O;KACF;IACD;;;OAGG;IACH,KAAK;QACH,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * COPIES WITH NO LENGTH BOUND (CWE-120)
3
+ *
4
+ * READ THE NAME OF THIS RULE CAREFULLY. It is not `buffer-overflow`, and the
5
+ * difference is the entire honesty argument for shipping it.
6
+ *
7
+ * A buffer overflow is a SIZE bug. Whether `strcpy(dst, src)` overflows depends
8
+ * on how big `dst` is, how long `src` turns out to be at runtime, and whether
9
+ * any pointer between them aliases something else. This engine models none of
10
+ * that - it tracks VALUES, not sizes, allocation lifetimes or aliasing. It
11
+ * cannot tell you whether a copy overflows and does not claim to.
12
+ *
13
+ * What it CAN prove from data flow alone is narrower and still worth saying:
14
+ *
15
+ * attacker-controlled data reaches a function that writes until it
16
+ * meets a NUL byte, and the attacker chooses where that byte is.
17
+ *
18
+ * That is what a finding here asserts, and the wording says so. A `strcpy` into
19
+ * a destination that genuinely is large enough is a FALSE POSITIVE of this
20
+ * rule. That is declared here, in the finding text and in the coverage report,
21
+ * rather than left for somebody to discover and lose trust over.
22
+ *
23
+ * It is still worth reporting, because the unbounded functions have no safe
24
+ * upper bound by construction. Every one of them has a bounded sibling that
25
+ * takes a length - strncpy, strncat, snprintf, strlcpy - and those are
26
+ * registered as sanitisers for this sink kind, so switching to them retracts
27
+ * the finding. A rule that cannot be satisfied by fixing the code is a rule
28
+ * nobody will act on.
29
+ *
30
+ * gets() IS THE EXCEPTION, and it needs no data flow at all. It has no length
31
+ * parameter and no way to acquire one, which is why C11 removed it from the
32
+ * standard library outright. There is nothing to trace, so it is reported on
33
+ * sight as a SIGNATURE finding - honestly labelled as unverified flow, because
34
+ * there is no flow to verify, not because we failed to find one.
35
+ */
36
+ import { type Rule } from './contract.js';
37
+ export declare const unboundedCopyRule: Rule;