@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,511 @@
1
+ /**
2
+ * TAINT ANALYSIS - THE VOCABULARY
3
+ * ===============================
4
+ *
5
+ * "Taint" is the oldest metaphor in program analysis and still the clearest:
6
+ * data that came from outside your program is DIRTY. Dirt spreads when you copy
7
+ * or combine the value. Washing it is possible, but only with the right soap for
8
+ * the right job. A vulnerability is dirt arriving somewhere dangerous unwashed.
9
+ *
10
+ * Four words, and the whole engine is built out of them:
11
+ *
12
+ * SOURCE where dirt enters. `req.query.id`, `request.form['x']`,
13
+ * `sys.argv`. Someone outside your program chose this value.
14
+ *
15
+ * PROPAGATION dirt spreading. `const b = a;` `"x" + a` `a.trim()` `f"{a}"`.
16
+ * A copy of a dirty value is dirty. So is a string containing it.
17
+ *
18
+ * SANITISER washing. `html.escape(a)` for HTML, `shlex.quote(a)` for shells,
19
+ * `parseInt(a)` for anything that should be a number.
20
+ *
21
+ * SINK where dirt does damage. `db.query(...)`, `exec(...)`,
22
+ * `el.innerHTML = ...`. These read strings as instructions.
23
+ *
24
+ * THE PART MOST TOOLS GET WRONG - soap is job-specific.
25
+ *
26
+ * `escapeHtml(x)` makes a value safe to put in a web page. It does absolutely
27
+ * nothing to make it safe inside a SQL query - `<` is not valid SQL escaping,
28
+ * and the dangerous characters for SQL (quotes) are not the dangerous characters
29
+ * for HTML (angle brackets). A scanner that keeps one boolean "is it clean?"
30
+ * will silently accept HTML-escaping in front of a database call.
31
+ *
32
+ * So taint here carries a SET of kinds it has been cleaned for, and a sink only
33
+ * accepts soap matching its own kind. That's the `sanitizedFor` field below, and
34
+ * it is a small design decision with a large correctness payoff.
35
+ */
36
+
37
+ import type { FlowStep } from '../core/finding.js';
38
+
39
+ /** The categories of damage a dirty value can do. */
40
+ export type SinkKind =
41
+ | 'sql'
42
+ | 'command'
43
+ | 'xss'
44
+ | 'deserialization'
45
+ | 'code'
46
+ | 'ssrf'
47
+ /**
48
+ * A tainted FILE PATH (CWE-22). `../` in the value walks out of the folder
49
+ * the code meant, so a read returns any file the server can see and a write
50
+ * or delete reaches any file it can touch. Separate from `ssrf` because PHP's
51
+ * file_get_contents is both: a URL there is a request, a path a file read.
52
+ */
53
+ | 'path'
54
+ /**
55
+ * A tainted FORMAT STRING (CWE-134). C-family only, and the cleanest sink in
56
+ * this union: `printf(name)` reads the attacker's string as a format, so
57
+ * `%n` turns a logging call into an arbitrary memory write. It is separate
58
+ * from `command` because the fix is different (make the format constant) and
59
+ * separate from `xss` because nothing is being rendered.
60
+ */
61
+ | 'format'
62
+ /**
63
+ * A tainted value reaching a copy with NO LENGTH BOUND (CWE-120).
64
+ *
65
+ * Read the name carefully: it is not `buffer-overflow`. Whether a copy
66
+ * actually overflows depends on the destination's size, which this engine
67
+ * does not model - it tracks values, not sizes. What is provable from data
68
+ * flow alone is that attacker-controlled data reached a function that writes
69
+ * until it meets a NUL byte, with the attacker choosing where that is. The
70
+ * finding says exactly that and no more.
71
+ */
72
+ | 'overflow';
73
+
74
+ export const SINK_KIND_RULE: Record<SinkKind, string> = {
75
+ sql: 'sql-injection',
76
+ command: 'command-injection',
77
+ xss: 'xss',
78
+ deserialization: 'unsafe-deserialization',
79
+ code: 'code-injection',
80
+ ssrf: 'ssrf',
81
+ path: 'path-traversal',
82
+ format: 'format-string',
83
+ overflow: 'unbounded-copy',
84
+ };
85
+
86
+ /**
87
+ * What we know about one value.
88
+ *
89
+ * There is no `tainted: false` state - an untainted value is simply `null`.
90
+ * Modelling "clean" as the absence of a record keeps the engine honest: we only
91
+ * ever claim dirt we can trace, never cleanliness we assumed.
92
+ */
93
+ export interface Taint {
94
+ /** Every hop so far, starting with the source. Becomes the finding's flowPath. */
95
+ readonly steps: readonly FlowStep[];
96
+ /** Kinds of sink this value has been properly washed for. */
97
+ readonly sanitizedFor: ReadonlySet<SinkKind>;
98
+ /** Short label for the origin, e.g. "req.query.id". Used in messages. */
99
+ readonly origin: string;
100
+ }
101
+
102
+ /** A pattern that means "an attacker chose this value". */
103
+ export interface SourceSpec {
104
+ /** Matched against the source text of an expression. */
105
+ readonly pattern: RegExp;
106
+ /** Plain-language description for the flow path, e.g. "HTTP query string". */
107
+ readonly description: string;
108
+ /**
109
+ * The WHOLE expression, anchored, that `pattern` also matches but that the
110
+ * client does not write - Django's `request.META["CSRF_COOKIE"]`, set by its
111
+ * own middleware after checking the cookie is letters and digits. Such an
112
+ * expression is not a source, and the container it is read from does not
113
+ * make it dirty either. Keep these exact: anything not listed stays a source.
114
+ */
115
+ readonly except?: RegExp;
116
+ /**
117
+ * A source that exists only in a web browser - the address bar, browser
118
+ * storage, document.cookie. Code that reads one runs in the user's own
119
+ * browser, so a request it makes is the browser's, not a server's, and
120
+ * cannot be server-side request forgery. See `fixedDestination` in tracer.ts.
121
+ */
122
+ readonly browserOnly?: boolean;
123
+ /**
124
+ * A source the person RUNNING the program writes - its command line, its
125
+ * environment, the terminal. Opening the file `argv[1]` names is what a
126
+ * command-line tool is for, so a path built from one is not proven path
127
+ * traversal: whoever chose it can already read that file. See `localSource`
128
+ * in tracer.ts. Every other kind is unaffected.
129
+ */
130
+ readonly localOnly?: boolean;
131
+ /**
132
+ * A file name the platform has already cut down to its last part. PHP keeps
133
+ * only what follows the last `/` or `\` in an upload's name, so
134
+ * `$_FILES['f']['name']` cannot climb a folder - storing the upload under it
135
+ * is an unrestricted-upload question, not path traversal. Every other kind
136
+ * (an echoed `<script>` file name, say) is unaffected.
137
+ */
138
+ readonly fileNameOnly?: boolean;
139
+ }
140
+
141
+ /** A call that does something dangerous with a string. */
142
+ export interface CallSinkSpec {
143
+ readonly kind: SinkKind;
144
+ /** Method or function names, last segment only. */
145
+ readonly methods: readonly string[];
146
+ /**
147
+ * WHICH arguments are dangerous. This matters enormously: in
148
+ * `db.query("SELECT ... ?", [userId])` argument 0 is the instruction and
149
+ * argument 1 is data the driver keeps separate. Treating every argument as a
150
+ * sink would report the CORRECT, parameterised form as a vulnerability - the
151
+ * single worst false positive a SQL rule can produce, because it punishes the
152
+ * fix.
153
+ */
154
+ readonly argIndexes: readonly number[] | 'all';
155
+ /**
156
+ * Only a sink when the call sits on one of these receivers.
157
+ *
158
+ * `loads` and `load` are dangerous on `pickle` and meaningless on everything
159
+ * else, and every Python codebase has a settings loader. Matching the method
160
+ * name alone reported `json.loads(request.data)` - the SAFE parser - as a
161
+ * deserialization sink, which is the same class of mistake as flagging a
162
+ * parameterised query.
163
+ */
164
+ readonly requiredReceivers?: readonly string[];
165
+ /**
166
+ * Only a sink when the call has NO receiver at all.
167
+ *
168
+ * The mirror of requiredReceivers, for built-in functions. `compile(src)` is
169
+ * Python's builtin; `re.compile(pattern)` is a regular expression and shares
170
+ * only the name. See BARE_BUILTINS in rules/code-injection.ts.
171
+ */
172
+ readonly bareOnly?: boolean;
173
+ /** Receivers that keep a bareOnly sink alive, because they ARE the evaluator. */
174
+ readonly allowedReceivers?: readonly string[];
175
+ /**
176
+ * Only a sink when the receiver EXPRESSION matches this.
177
+ *
178
+ * The looser sibling of requiredReceivers, for receivers that are themselves
179
+ * calls: `response.getWriter().write(x)` has no single name to compare.
180
+ *
181
+ * This is what replaced `contentCheck: 'html'` on the Java response sinks.
182
+ * The content check was there to stop `sb.append(x)` reading as XSS, but it
183
+ * demanded that the ARGUMENT look like HTML - and the argument is usually an
184
+ * opaque variable, so it rejected 204 real cases to avoid one false one.
185
+ * Asking about the RECEIVER instead separates them exactly: a servlet writer
186
+ * writes to the page, a StringBuilder does not, whatever is passed to either.
187
+ */
188
+ readonly receiverPattern?: RegExp;
189
+ /**
190
+ * For sinks whose DESTINATION is argument zero rather than the receiver.
191
+ *
192
+ * `fmt.Fprintf(w, format, args...)` writes to `w`. The receiver is `fmt`,
193
+ * which says nothing at all, so `receiverPattern` cannot reach this shape -
194
+ * and it is the one shape in the sink table that stayed wrong through a whole
195
+ * scoping sweep, still reporting `fmt.Fprintf(out, ...)` in a test mock as
196
+ * cross-site scripting.
197
+ *
198
+ * When set, argument zero's text (or its declared type) must match, AND
199
+ * argument zero is excluded from the tainted-value check - it is where the
200
+ * output goes, not what is written.
201
+ */
202
+ readonly writerArgPattern?: RegExp;
203
+ /** Only a sink when the call text also shows a shell being used. */
204
+ readonly requiresShellOption?: RegExp;
205
+ /**
206
+ * NOT a sink when the call text shows the dangerous behaviour turned off.
207
+ *
208
+ * The mirror image of requiresShellOption, and it exists because the safe
209
+ * form of a call is often the dangerous call plus an argument:
210
+ *
211
+ * unserialize($data, ['allowed_classes' => false]) // PHP
212
+ * yaml.load(text, Loader=yaml.SafeLoader) // Python
213
+ * HttpResponse(body, content_type="text/plain") // Django: not a page
214
+ *
215
+ * Both are still a call to the risky function. Reporting them would punish
216
+ * the fix, which is the worst thing an injection rule can do - it teaches
217
+ * people that hardening does not help.
218
+ *
219
+ * This reads the VISIBLE TEXT of the call, so an options object assembled in
220
+ * another variable is missed and the finding still fires. That errs toward
221
+ * reporting, which is the safe direction, and it is stated in the rule's
222
+ * limitations rather than left for someone to discover.
223
+ */
224
+ readonly unlessOption?: RegExp;
225
+ /**
226
+ * Only a sink when the argument's literal text actually looks like this kind
227
+ * of content. Needed because method names collide across libraries: `exec` is
228
+ * both a database call and a shell call, and `append` is both a jQuery HTML
229
+ * insertion and an ordinary array method. Without this, `exec("ping " + host)`
230
+ * gets reported as SQL injection - which it is not.
231
+ */
232
+ readonly contentCheck?: 'sql' | 'html';
233
+ readonly description: string;
234
+ }
235
+
236
+ /** An assignment target that does something dangerous with a string. */
237
+ export interface AssignSinkSpec {
238
+ readonly kind: SinkKind;
239
+ /** Property names, last segment only: innerHTML, __html, srcdoc. */
240
+ readonly properties: readonly string[];
241
+ /**
242
+ * Require the assigned text to LOOK like the thing this sink parses.
243
+ *
244
+ * `innerHTML` needs no such check - the property name is the proof, because
245
+ * the browser will parse whatever lands there. A PHP page buffer is the
246
+ * opposite: `$html`, `$output` and `$content` are conventions, and
247
+ * `$sql .= $where` accumulates a query rather than a page. Requiring visible
248
+ * markup is what separates the two without inventing a type system.
249
+ */
250
+ readonly contentCheck?: 'sql' | 'html';
251
+ readonly description: string;
252
+ }
253
+
254
+ /** A function that makes a value safe - for SOME kinds of sink, not all. */
255
+ export interface SanitizerSpec {
256
+ readonly names: readonly string[];
257
+ /** Which sinks this soap actually works for. */
258
+ readonly kinds: readonly SinkKind[];
259
+ /**
260
+ * Kinds this soap protects ONLY when its output lands inside a quoted string.
261
+ *
262
+ * `mysqli_real_escape_string()` puts a backslash in front of quote characters
263
+ * and nothing else. Inside `'...'` in a query that is the whole defence - a
264
+ * quote is the only way out. In an unquoted slot (`WHERE id = $id`) there is
265
+ * nothing to break out of, and `1 OR 1=1` passes through it untouched. So the
266
+ * value is only provisionally clean until the tracer sees where it is spliced
267
+ * into the query. See `quotePending` in tracer.ts.
268
+ */
269
+ readonly onlyInsideQuotes?: readonly SinkKind[];
270
+ /**
271
+ * The entry holds only for calls with at most this many arguments.
272
+ *
273
+ * `md5($x)` returns 32 hex characters, which cannot carry a quote or a tag.
274
+ * `md5($x, true)` returns 16 RAW bytes, which can - `'ffifdyop'` hashes to a
275
+ * string containing `'or'6`, the well-known login bypass. Same name, one
276
+ * extra argument, opposite answer. A call with more arguments than this is
277
+ * not matched by the entry and is treated as a function we do not model.
278
+ */
279
+ readonly maxArgs?: number;
280
+ /**
281
+ * The entry holds only when the FIRST argument's text matches. Node's
282
+ * `hash.digest('hex')` returns hex digits; `hash.digest()` returns raw bytes,
283
+ * which can hold any character at all. Same name, the argument decides.
284
+ */
285
+ readonly argumentPattern?: RegExp;
286
+ /**
287
+ * The value being washed is the RECEIVER and the arguments are options:
288
+ * `crypto.createHash(...).update(dirty).digest('hex')`.
289
+ */
290
+ readonly fromReceiver?: boolean;
291
+ readonly description: string;
292
+ }
293
+
294
+ /**
295
+ * Functions whose RESULT is new data rather than their argument.
296
+ *
297
+ * The default for a function this engine does not know is that it hands its
298
+ * argument back - right for `stripslashes()`, and how the tool finds anything
299
+ * at all in real code. It is wrong for a read: `mysqli_query($conn, $sql)`
300
+ * returns rows stored in the database, not the text of `$sql`, and a proof
301
+ * that runs from the request through the query into the printed row is a
302
+ * false statement about the code. An AI review of DVWA's published proofs
303
+ * marked every one of those paths wrong.
304
+ *
305
+ * The trace stops at these calls. That is NOT a claim the result is safe -
306
+ * stored data can be just as dangerous, it is just not THIS request's input -
307
+ * so nothing is cleared: a sink further on is still reported by its signature
308
+ * rule, unproven, and every stop is listed in diagnostics.tracesStoppedAtNewData.
309
+ *
310
+ * Consulted only for a call this engine cannot read: a function of the same
311
+ * name defined in the project is followed instead.
312
+ */
313
+ export interface FreshResultSpec {
314
+ readonly names: readonly string[];
315
+ /** What comes back instead of the argument. Printed in the stop record. */
316
+ readonly description: string;
317
+ /**
318
+ * A database read, where an INJECTABLE query hands the attacker the rows.
319
+ *
320
+ * DVWA's brute/source/low.php puts the raw username into the query, then
321
+ * prints the avatar column of the row it returns. The row is not the
322
+ * username - but `' UNION SELECT ...` makes it whatever the attacker likes,
323
+ * and an AI review called that proof real for exactly that reason. The two
324
+ * DVWA levels that escape the username inside quotes return stored rows
325
+ * only, and the same review called those proofs wrong.
326
+ *
327
+ * So for these entries an ARGUMENT that is still dirty for SQL is carried
328
+ * into the result, with a step saying why. The query functions watch the
329
+ * same argument positions as their SQL sink entry; the row readers watch
330
+ * their arguments - a result handle - and never the receiver, because a
331
+ * statement dirtied by bindParam() holds bound parameters, which cannot
332
+ * inject anything.
333
+ */
334
+ readonly rowsFollowInjection?: boolean;
335
+ }
336
+
337
+ /** A function whose output is still dirty if its input was. */
338
+ export interface PropagatorSpec {
339
+ readonly names: readonly string[];
340
+ }
341
+
342
+ export interface TaintDictionary {
343
+ readonly sources: readonly SourceSpec[];
344
+ /**
345
+ * Sources that are PARAMETER DECLARATIONS rather than expressions.
346
+ *
347
+ * Every other source in this file is recognised by the text of an expression:
348
+ * `req.query.id` looks like a query parameter wherever it appears. A Spring
349
+ * handler has no such expression - the framework binds the request value to a
350
+ * parameter NAME before the method body starts, and the only evidence in the
351
+ * source is an annotation on the declaration:
352
+ *
353
+ * public String lookup(@RequestParam String account) { ... }
354
+ *
355
+ * `account` is attacker-controlled from its first use and nothing in the body
356
+ * says so. Matching the declaration is the only way to see it.
357
+ */
358
+ readonly parameterSources?: readonly SourceSpec[];
359
+ readonly callSinks: readonly CallSinkSpec[];
360
+ readonly assignSinks: readonly AssignSinkSpec[];
361
+ readonly sanitizers: readonly SanitizerSpec[];
362
+ readonly propagators: PropagatorSpec;
363
+ /**
364
+ * Methods where a tainted ARGUMENT dirties the RECEIVER.
365
+ *
366
+ * sb.append(param); // the taint lands on `sb`, not on a return
367
+ * argList.add(param); // `add` returns a boolean
368
+ *
369
+ * Propagators are value-in / value-out: they carry taint through a return
370
+ * value that someone assigns. These do not. `append` and `format` were both
371
+ * already in the Java propagator list and still missed every case, because
372
+ * the return is thrown away and the taint has to attach to the object.
373
+ *
374
+ * Measured on OWASP BenchmarkJava, this one shape is about 60% of all false
375
+ * negatives - 49 StringBuilder cases plus 151 that push a request value into
376
+ * a List or Map before the sink. It is the single largest recall gap, and it
377
+ * is language-agnostic.
378
+ */
379
+ readonly mutators?: readonly string[];
380
+ /**
381
+ * Functions where a tainted argument dirties argument ZERO.
382
+ *
383
+ * `mutators` above binds taint to the receiver, which is where the filled
384
+ * object sits in every language that has receivers. C has none: the
385
+ * destination is the first argument.
386
+ *
387
+ * sprintf(cmd, "ping %s", argv[1]); -> `cmd` is now tainted
388
+ * strcpy(buf, argv[1]); -> `buf` is now tainted
389
+ *
390
+ * Without this, C coverage is close to zero - the build-then-run pattern is
391
+ * how every command injection in the language is actually written, and the
392
+ * receiver rule cannot see it. See checkOutParamMutation in tracer.ts.
393
+ */
394
+ readonly outParamMutators?: readonly string[];
395
+ /**
396
+ * Functions that ARE a source and deliver their bytes through a parameter.
397
+ *
398
+ * fgets(line, sizeof(line), stdin); -> `line` is now attacker input
399
+ * read(fd, buf, n); -> `buf` is
400
+ * scanf("%s", name); -> `name` is
401
+ *
402
+ * The ordinary source list matches the TEXT of an expression, which works
403
+ * when the value is the call's return - `getenv("X")` reads as a source
404
+ * wherever it appears. These functions return a count or a status; the data
405
+ * goes into an argument, and the call is usually a statement whose result
406
+ * nobody keeps. So there is no expression to match and nothing to bind,
407
+ * unless the destination index is known - and it differs per function, which
408
+ * is why this carries one rather than assuming zero.
409
+ */
410
+ readonly outParamSources?: readonly {
411
+ readonly names: readonly string[];
412
+ /** Which argument receives the bytes. */
413
+ readonly destination: number;
414
+ readonly description: string;
415
+ }[];
416
+ /**
417
+ * KEYED mutators - a write into a named compartment of an object that is
418
+ * primarily something else.
419
+ *
420
+ * req.setAttribute("loginForm", dirty);
421
+ *
422
+ * These are mutators too, and they appear in `mutators` as well; this list
423
+ * says something extra about a subset of them. The plain kind ARE the value
424
+ * they were handed: after `sb.append(dirty)`, `sb` is the dirty string and
425
+ * every read of `sb` is a read of it. A servlet request is not its attribute
426
+ * map. It also has a method, a URI, a session and a context path, and none
427
+ * of those changed because something was filed in a side compartment.
428
+ *
429
+ * Scanning Jenkins produced a flow-verified finding on `req.getContextPath()`
430
+ * for exactly this reason: an unmodelled call carries its receiver's taint,
431
+ * the receiver had been dirtied by setAttribute, so a container-configured
432
+ * constant was reported as attacker-controlled. Worse, the tracer then
433
+ * attributed the flow to the one argument the code did NOT escape, so the
434
+ * escaper retraction added a version earlier never fired.
435
+ *
436
+ * The rule this enables is narrow: taint written through a keyed mutator
437
+ * comes back out through `keyedReaders`, and not through anything else.
438
+ * Reads out of the store stay deliberately KEY-INSENSITIVE - `setAttribute`
439
+ * under one name and `getAttribute` under another is still treated as
440
+ * tainted, because matching key literals would be a second guess layered on
441
+ * a first. The imprecision is aimed, not removed.
442
+ */
443
+ readonly keyedMutators?: readonly string[];
444
+ /**
445
+ * ELEMENT mutators - the subset of `mutators` that add one SEPARABLE ITEM to
446
+ * a collection, as opposed to accumulating into a single value.
447
+ *
448
+ * list.add(dirty) one element among several. A later read picks
449
+ * ONE of them, and we do not model which.
450
+ * sb.append(dirty) not separable. Every appended piece is in the
451
+ * result, so any read of `sb` reads the dirty one.
452
+ *
453
+ * This list exists only to decide a CONFIDENCE LABEL, never to drop taint.
454
+ * When a collection took more than one element write, a read of it might be
455
+ * any of them, so the tracer marks the flow a guess and the finding is
456
+ * emitted signature-based. One write means the value read is the value
457
+ * written, and the proof stands.
458
+ *
459
+ * Measured on OWASP BenchmarkJava, flow-verified findings only: paths
460
+ * collecting via `add()` scored 49.1% precision and via `put()` 53.2%, next
461
+ * to 71.5% for paths with no collect step at all. A coin flip cannot call
462
+ * itself a proven trace.
463
+ *
464
+ * THE NAMES ARE NOT PORTABLE, which is the whole reason this is per-language
465
+ * rather than one shared list. Java's `append` is StringBuilder concatenation
466
+ * and must stay OUT; Python's `append` is a list element and must be IN. A
467
+ * copied list would silently downgrade real findings in one language and
468
+ * overclaim in another.
469
+ */
470
+ readonly elementMutators?: readonly string[];
471
+ /**
472
+ * The methods that legitimately read a keyed store back out. Only consulted
473
+ * for taint that arrived via `keyedMutators`; every other method on such an
474
+ * object is treated as untouched by the write.
475
+ */
476
+ readonly keyedReaders?: readonly string[];
477
+ /**
478
+ * Sinks fired by a tainted RECEIVER rather than a tainted argument.
479
+ *
480
+ * pb.command(argList); // argList is dirty, so pb is now dirty
481
+ * pb.start(); // <- the dangerous act, and it takes NO arguments
482
+ *
483
+ * Every other sink check in this engine reads the argument list. This shape
484
+ * has an empty one: the payload was loaded into the object on an earlier
485
+ * line, and the call that executes it carries nothing. It is the standard
486
+ * ProcessBuilder idiom and it is all over OWASP BenchmarkJava.
487
+ */
488
+ readonly receiverSinks?: readonly {
489
+ readonly methods: readonly string[];
490
+ readonly kind: SinkKind;
491
+ readonly description: string;
492
+ }[];
493
+ /**
494
+ * `return "<h1>" + name + "</h1>"` is a sink in this language.
495
+ *
496
+ * In Flask and Django a view that returns a string HAS returned the response
497
+ * body - there is no `res.send()` to match on, so the dangerous call this
498
+ * engine looks for simply is not written anywhere. A tester's fixture caught
499
+ * this: an obvious reflected XSS produced nothing, and - worse - the ESCAPED
500
+ * version of the same code "passed" for the wrong reason, because with no
501
+ * sink recognised there was nothing for `html.escape` to be credited against.
502
+ *
503
+ * Set only where returning a string plausibly means returning a page. Express
504
+ * is deliberately excluded: it answers with `res.send`, which is already a
505
+ * modelled call sink, so enabling this there would invent a second sink for
506
+ * the same thing.
507
+ */
508
+ readonly htmlReturnIsSink?: boolean;
509
+ /** Reads whose result is new data, not the argument - see FreshResultSpec. */
510
+ readonly freshResults?: readonly FreshResultSpec[];
511
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * WHERE A DECLARATION CAN BE SEEN, AND WHAT THE PARSER CANNOT SEE.
3
+ * ===============================================================
4
+ *
5
+ * The constant proof (rules/lib/strings.ts) collects every declaration of a
6
+ * name in the scopes around a read. A collected declaration is only the one the
7
+ * read means if the read is INSIDE where that declaration is visible - and
8
+ * every language here scopes more tightly than "somewhere in this function".
9
+ * The second break-it round (tests/probes/g05, s02, s03, s05, s06) was a
10
+ * literal declared in a block, a `for` header, a class static block or a
11
+ * namespace, standing in for the import or package variable the read really
12
+ * meant.
13
+ *
14
+ * Every answer here may be NARROWER than the language's real rule - then the
15
+ * proof gives up and the finding stays - but never wider.
16
+ */
17
+
18
+ import type { Node } from 'web-tree-sitter';
19
+ import type { LanguageId } from '../parse/languages.js';
20
+ import { declarationScope } from './java-scope.js';
21
+
22
+ /** From this offset to the end of `region`, the declaration is the one a read of its name means. */
23
+ export interface Visibility {
24
+ readonly region: Node;
25
+ readonly from: number;
26
+ }
27
+
28
+ const JS_FUNCTIONS = new Set([
29
+ 'function_declaration', 'function_expression', 'arrow_function', 'method_definition',
30
+ 'generator_function_declaration', 'generator_function', 'class_static_block', 'program',
31
+ ]);
32
+
33
+ export function declarationVisibility(declarator: Node, language: LanguageId): Visibility | null {
34
+ if (language === 'java') return declarationScope(declarator);
35
+
36
+ if (language === 'javascript' || language === 'typescript') {
37
+ const declaration = declarator.parent;
38
+ if (!declaration) return null;
39
+ let holder = declaration.parent;
40
+ while (holder?.type === 'export_statement') holder = holder.parent;
41
+ if (!holder) return null;
42
+ // `let` and `const`: the enclosing block (a `for` header's: the loop),
43
+ // from the declaration on - before it is the temporal dead zone.
44
+ if (declaration.type === 'lexical_declaration') return { region: holder, from: declarator.endIndex };
45
+ // `var` is hoisted to the top of its function.
46
+ if (declaration.type === 'variable_declaration') {
47
+ let fn: Node | null = holder;
48
+ while (fn && !JS_FUNCTIONS.has(fn.type)) fn = fn.parent;
49
+ return fn ? { region: fn, from: fn.startIndex } : null;
50
+ }
51
+ return null;
52
+ }
53
+
54
+ if (language === 'go') {
55
+ if (declarator.type === 'short_var_declaration') {
56
+ // `x := v` is visible to the end of its block; in an `if`, `for` or
57
+ // `switch` header, to the end of that statement.
58
+ let holder = declarator.parent;
59
+ if (holder?.type === 'for_clause') holder = holder.parent;
60
+ return holder ? { region: holder, from: declarator.endIndex } : null;
61
+ }
62
+ if (declarator.type === 'var_spec') {
63
+ let declaration = declarator.parent;
64
+ if (declaration?.type === 'var_spec_list') declaration = declaration.parent;
65
+ const holder = declaration?.parent;
66
+ if (!holder) return null;
67
+ // A package-level `var` is visible in the whole package, in any order.
68
+ if (holder.type === 'source_file') return { region: holder, from: holder.startIndex };
69
+ return { region: holder, from: declarator.endIndex };
70
+ }
71
+ return null;
72
+ }
73
+
74
+ return null;
75
+ }
76
+
77
+ /*
78
+ * TEXT THE COMPILER READS AND THE PARSER DOES NOT.
79
+ *
80
+ * A lone carriage return ends a `//` comment for javac, for JavaScript
81
+ * engines and for Python's tokenizer; U+2028 and U+2029 end one in
82
+ * JavaScript; `\u000a` is a newline to javac before it parses anything.
83
+ * Tree-sitter reads all of them as more comment - so `// note<CR> x = f();`
84
+ * hides a real write (tests/probes/j15, s10, p31, p32). A file holding any of
85
+ * them is a file the constant proof does not try to read.
86
+ */
87
+ const hidden = new WeakMap<object, boolean>();
88
+ export function hidesCodeFromParser(n: Node, language: LanguageId): boolean {
89
+ const key = n.tree as object;
90
+ let found = hidden.get(key);
91
+ if (found === undefined) {
92
+ const text = n.tree.rootNode.text ?? '';
93
+ found =
94
+ /\r(?!\n)/.test(text) ||
95
+ ((language === 'javascript' || language === 'typescript') && new RegExp("[\\u2028\\u2029]").test(text)) ||
96
+ (language === 'java' && /\\u+[0-9a-fA-F]{4}/.test(text));
97
+ hidden.set(key, found);
98
+ }
99
+ return found;
100
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ // ---------------------------------------------------------------------------
3
+ // TypeScript compiler settings for Defuse.
4
+ // Every option below is here for a reason; the comments say what that reason is.
5
+ // ---------------------------------------------------------------------------
6
+ "compilerOptions": {
7
+ // "What JavaScript features may the output use?"
8
+ // Node 20+ supports everything in ES2022, so we don't need TypeScript to
9
+ // rewrite modern syntax into older syntax. Less rewriting = output that
10
+ // looks like the code you wrote, which matters when you're learning.
11
+ "target": "ES2022",
12
+ "lib": ["ES2022"],
13
+
14
+ // "How do imports work?" NodeNext tells TypeScript to use Node's real,
15
+ // modern module rules (import/export, file extensions in paths). This is
16
+ // the setting that matches "type": "module" in package.json.
17
+ "module": "NodeNext",
18
+ "moduleResolution": "NodeNext",
19
+
20
+ // Where the .ts files are and where the compiled .js files go.
21
+ // rootDir "." means both src/ and tests/ are compiled, preserving structure:
22
+ // src/cli.ts -> dist/src/cli.js
23
+ // tests/run-tests.ts -> dist/tests/run-tests.js
24
+ "rootDir": ".",
25
+ "outDir": "dist",
26
+
27
+ // STRICT MODE. This is the single most valuable setting for a beginner.
28
+ // It makes TypeScript refuse to compile code where a value might be
29
+ // undefined/null and you forgot to handle it. It turns whole categories of
30
+ // "why is this crashing at 2am" into "why is this red squiggle here".
31
+ "strict": true,
32
+
33
+ // Extra strictness that strict:true does NOT include but we want:
34
+ // - noUncheckedIndexedAccess: reading arr[5] gives you "T | undefined",
35
+ // forcing you to handle the case where index 5 doesn't exist. Essential
36
+ // for a scanner that indexes into AST children constantly.
37
+ // - exactOptionalPropertyTypes: "optional" means absent, not "maybe undefined".
38
+ // - noImplicitOverride / noFallthroughCasesInSwitch: catch classic slips.
39
+ "noUncheckedIndexedAccess": true,
40
+ "exactOptionalPropertyTypes": true,
41
+ "noImplicitOverride": true,
42
+ "noFallthroughCasesInSwitch": true,
43
+ "noUnusedLocals": true,
44
+ "noUnusedParameters": true,
45
+
46
+ // Emit .d.ts type declarations and source maps. Source maps mean that when
47
+ // the compiled JS throws an error, the stack trace points at the .ts line
48
+ // you actually wrote instead of the generated one.
49
+ "declaration": true,
50
+ "sourceMap": true,
51
+
52
+ // Interop + hygiene.
53
+ "esModuleInterop": true,
54
+ "forceConsistentCasingInFileNames": true,
55
+ "skipLibCheck": true,
56
+ "resolveJsonModule": true
57
+ },
58
+ "include": ["src/**/*.ts", "tests/**/*.ts"],
59
+ // Fixtures and probes are code for the scanner to READ, not to compile.
60
+ "exclude": ["node_modules", "dist", "tests/fixtures", "tests/probes"]
61
+ }