circle-ir 3.54.0 → 3.55.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 (32) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +36 -3
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/findings.d.ts.map +1 -1
  5. package/dist/analysis/findings.js +11 -6
  6. package/dist/analysis/findings.js.map +1 -1
  7. package/dist/analysis/passes/csrf-protection-disabled-pass.d.ts +42 -0
  8. package/dist/analysis/passes/csrf-protection-disabled-pass.d.ts.map +1 -0
  9. package/dist/analysis/passes/csrf-protection-disabled-pass.js +185 -0
  10. package/dist/analysis/passes/csrf-protection-disabled-pass.js.map +1 -0
  11. package/dist/analysis/passes/mass-assignment-pass.d.ts +41 -0
  12. package/dist/analysis/passes/mass-assignment-pass.d.ts.map +1 -0
  13. package/dist/analysis/passes/mass-assignment-pass.js +124 -0
  14. package/dist/analysis/passes/mass-assignment-pass.js.map +1 -0
  15. package/dist/analysis/passes/xml-entity-expansion-pass.d.ts +58 -0
  16. package/dist/analysis/passes/xml-entity-expansion-pass.d.ts.map +1 -0
  17. package/dist/analysis/passes/xml-entity-expansion-pass.js +196 -0
  18. package/dist/analysis/passes/xml-entity-expansion-pass.js.map +1 -0
  19. package/dist/analysis/rules.d.ts.map +1 -1
  20. package/dist/analysis/rules.js +18 -0
  21. package/dist/analysis/rules.js.map +1 -1
  22. package/dist/analysis/taint-propagation.js +1 -1
  23. package/dist/analysis/taint-propagation.js.map +1 -1
  24. package/dist/analyzer.d.ts.map +1 -1
  25. package/dist/analyzer.js +9 -0
  26. package/dist/analyzer.js.map +1 -1
  27. package/dist/browser/circle-ir.js +389 -11
  28. package/dist/core/circle-ir-core.cjs +40 -5
  29. package/dist/core/circle-ir-core.js +40 -5
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/index.d.ts.map +1 -1
  32. package/package.json +1 -1
@@ -11177,9 +11177,16 @@ var DEFAULT_SINKS = [
11177
11177
  { method: "println", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
11178
11178
  // XSS in error messages (CWE-81)
11179
11179
  { method: "sendError", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11180
- // Response header injection (can lead to header XSS)
11181
- { method: "setHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11182
- { method: "addHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
11180
+ // Response header injection re-categorised from `xss` to `crlf`
11181
+ // (CWE-113) in Sprint 6 of #86. Header injection is HTTP response
11182
+ // splitting / cache-poisoning / cookie forging; reflected XSS via header
11183
+ // reflection remains a downstream concern of body-writing sinks.
11184
+ { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
11185
+ { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
11186
+ // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
11187
+ // (CWE-601) further down — see entry near line 1195. CRLF via Location
11188
+ // header is a secondary concern; keeping the canonical SSRF entry avoids
11189
+ // double-emission that would mask the open-redirect chain.
11183
11190
  { method: "setContentType", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
11184
11191
  // JSP output
11185
11192
  { method: "setAttribute", class: "PageContext", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
@@ -12148,7 +12155,33 @@ var DEFAULT_SINKS = [
12148
12155
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12149
12156
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12150
12157
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
12151
- { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
12158
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
12159
+ // CRLF / HTTP response splitting (CWE-113) — Sprint 6, #86.
12160
+ // Node.js / Express response header / cookie sinks. The header *name* (arg 0)
12161
+ // is also CRLF-sensitive but is almost always a string literal; we model
12162
+ // arg 1 (the value) as the primary sink.
12163
+ { method: "setHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
12164
+ { method: "writeHead", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [2], languages: ["javascript", "typescript"] },
12165
+ // Express: res.cookie(name, value, options) — value is CRLF-sensitive.
12166
+ { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
12167
+ // Express: res.location(url) and res.redirect(url) — Location header.
12168
+ { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12169
+ { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
12170
+ // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
12171
+ // (Header is a map; the actual `value` is arg 1 of the call). We flag the
12172
+ // value position so a tainted variable is detected.
12173
+ { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
12174
+ { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
12175
+ // Mass-assignment (CWE-915) — Sprint 6, #86.
12176
+ // JS Object.assign(target, ...sources) — sources are arg 1..N, and if any
12177
+ // source is request-tainted, every key gets written onto the target. We
12178
+ // flag the source positions; the analyzer only needs one tainted to fire.
12179
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
12180
+ // Lodash bulk-merge helpers behave identically.
12181
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
12182
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
12183
+ // jQuery $.extend(target, source) (legacy).
12184
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
12152
12185
  ];
12153
12186
  var DEFAULT_SANITIZERS = [
12154
12187
  // SQL Injection - proper parameter binding sanitizes input
@@ -13618,12 +13651,17 @@ function canSourceReachSink(sourceType, sinkType) {
13618
13651
  // code_injection added to http_param/http_query/http_header/http_cookie:
13619
13652
  // `eval(req.query.x)`, `Function(req.header('x'))`, `vm.runInThisContext(req.cookies.c)`
13620
13653
  // are all real RCE patterns in JS web apps (cognium-dev #83).
13621
- http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection"],
13622
- http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call"],
13623
- http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection"],
13624
- http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection"],
13654
+ // crlf added to http_param/http_query/http_header/http_cookie/http_body:
13655
+ // setHeader/setCookie/redirect of any user-controlled string is CRLF / response
13656
+ // splitting (CWE-113) Sprint 6, issue #86.
13657
+ // mass_assignment added to http_body / http_param: Object.assign(user, req.body),
13658
+ // User(**request.form) — CWE-915.
13659
+ http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
13660
+ http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
13661
+ http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf"],
13662
+ http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf"],
13625
13663
  http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call"],
13626
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection"],
13664
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
13627
13665
  io_input: ["command_injection", "path_traversal", "deserialization", "xxe", "code_injection", "xss"],
13628
13666
  env_input: ["command_injection", "path_traversal"],
13629
13667
  db_input: ["xss", "sql_injection"],
@@ -13632,7 +13670,7 @@ function canSourceReachSink(sourceType, sinkType) {
13632
13670
  network_input: ["sql_injection", "command_injection", "xss", "ssrf"],
13633
13671
  config_param: ["sql_injection", "command_injection", "path_traversal", "xss", "ssrf"],
13634
13672
  // Servlet init params
13635
- interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call"],
13673
+ interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
13636
13674
  // Cross-method taint
13637
13675
  plugin_param: ["sql_injection", "command_injection", "path_traversal", "xss", "code_injection"]
13638
13676
  // Plugin/config parameters
@@ -14833,7 +14871,9 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
14833
14871
  "code_injection",
14834
14872
  "mybatis_mapper_call",
14835
14873
  "redos",
14836
- "format_string"
14874
+ "format_string",
14875
+ "crlf",
14876
+ "mass_assignment"
14837
14877
  ]);
14838
14878
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
14839
14879
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -28087,6 +28127,341 @@ var JwtVerifyDisabledPass = class {
28087
28127
  }
28088
28128
  };
28089
28129
 
28130
+ // src/analysis/passes/csrf-protection-disabled-pass.ts
28131
+ var JAVA_CSRF_DISABLE_RE = /\.csrf\s*\([^)]*\)\s*\.\s*disable\b/;
28132
+ var JAVA_CSRF_LAMBDA_DISABLE_RE = /\bcsrf\s*\(\s*\w+\s*->\s*\w+\s*\.\s*disable\s*\(/;
28133
+ var JAVA_CSRF_METHODREF_RE = /\bcsrf\s*\(\s*[\w.]+::disable\s*\)/;
28134
+ var JAVA_CSRF_NULL_REPO_RE = /\.csrfTokenRepository\s*\(\s*null\s*\)/;
28135
+ var CsrfProtectionDisabledPass = class {
28136
+ name = "csrf-protection-disabled";
28137
+ category = "security";
28138
+ run(ctx) {
28139
+ const { graph, language } = ctx;
28140
+ const file = graph.ir.meta.file;
28141
+ const findings = [];
28142
+ for (const call of graph.ir.calls) {
28143
+ const detections = this.detectCall(call, language);
28144
+ for (const det of detections) {
28145
+ const line = call.location.line;
28146
+ findings.push({ line, language, ...det });
28147
+ ctx.addFinding({
28148
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28149
+ pass: this.name,
28150
+ category: this.category,
28151
+ rule_id: this.name,
28152
+ cwe: "CWE-352",
28153
+ severity: "critical",
28154
+ level: "error",
28155
+ message: `CSRF protection explicitly disabled via \`${det.pattern}\` (${det.api}). Any browser session can be silently used to perform state-changing requests from a malicious origin.`,
28156
+ file,
28157
+ line,
28158
+ fix: this.fixFor(language),
28159
+ evidence: { ...det, language }
28160
+ });
28161
+ }
28162
+ }
28163
+ if (language === "java") {
28164
+ const src = ctx.code ?? "";
28165
+ if (src) {
28166
+ const lines = src.split("\n");
28167
+ for (let i2 = 0; i2 < lines.length; i2++) {
28168
+ const line = i2 + 1;
28169
+ const text = lines[i2] ?? "";
28170
+ let det = null;
28171
+ if (JAVA_CSRF_LAMBDA_DISABLE_RE.test(text)) {
28172
+ det = { pattern: "csrf(c -> c.disable())", api: "HttpSecurity.csrf" };
28173
+ } else if (JAVA_CSRF_METHODREF_RE.test(text)) {
28174
+ det = { pattern: "csrf(::disable)", api: "HttpSecurity.csrf" };
28175
+ } else if (JAVA_CSRF_NULL_REPO_RE.test(text)) {
28176
+ det = { pattern: "csrfTokenRepository(null)", api: "HttpSecurity.csrfTokenRepository" };
28177
+ } else if (JAVA_CSRF_DISABLE_RE.test(text)) {
28178
+ det = { pattern: "csrf().disable()", api: "HttpSecurity.csrf" };
28179
+ }
28180
+ if (det && !findings.some((f) => f.line === line && f.pattern === det.pattern)) {
28181
+ findings.push({ line, language, ...det });
28182
+ ctx.addFinding({
28183
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28184
+ pass: this.name,
28185
+ category: this.category,
28186
+ rule_id: this.name,
28187
+ cwe: "CWE-352",
28188
+ severity: "critical",
28189
+ level: "error",
28190
+ message: `CSRF protection explicitly disabled via \`${det.pattern}\` (${det.api}). Any browser session can be silently used to perform state-changing requests from a malicious origin.`,
28191
+ file,
28192
+ line,
28193
+ fix: this.fixFor(language),
28194
+ evidence: { ...det, language }
28195
+ });
28196
+ }
28197
+ }
28198
+ }
28199
+ }
28200
+ if (language === "python") {
28201
+ const src = ctx.code ?? "";
28202
+ if (src) {
28203
+ const lines = src.split("\n");
28204
+ for (let i2 = 0; i2 < lines.length; i2++) {
28205
+ const text = lines[i2] ?? "";
28206
+ if (/^\s*@csrf_exempt\b/.test(text)) {
28207
+ const line = i2 + 1;
28208
+ const det = { pattern: "@csrf_exempt", api: "django.views.decorators.csrf" };
28209
+ findings.push({ line, language, ...det });
28210
+ ctx.addFinding({
28211
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28212
+ pass: this.name,
28213
+ category: this.category,
28214
+ rule_id: this.name,
28215
+ cwe: "CWE-352",
28216
+ severity: "critical",
28217
+ level: "error",
28218
+ message: "Django view is decorated with `@csrf_exempt`, bypassing the framework CSRF middleware for this endpoint. Any browser session can be silently used to invoke this handler from a malicious origin.",
28219
+ file,
28220
+ line,
28221
+ fix: this.fixFor(language),
28222
+ evidence: { ...det, language }
28223
+ });
28224
+ }
28225
+ }
28226
+ }
28227
+ }
28228
+ return { findings };
28229
+ }
28230
+ detectCall(call, language) {
28231
+ const out2 = [];
28232
+ if (language !== "java") return out2;
28233
+ if (call.method_name === "disable") {
28234
+ const recv = call.receiver ?? "";
28235
+ if (/\bcsrf\s*\(\s*\)\s*$/.test(recv) || recv.endsWith(".csrf()")) {
28236
+ out2.push({ pattern: "csrf().disable()", api: "HttpSecurity.csrf" });
28237
+ }
28238
+ }
28239
+ if (call.method_name === "csrfTokenRepository") {
28240
+ const arg = call.arguments.find((a) => a.position === 0);
28241
+ const expr = (arg?.expression ?? arg?.literal ?? "").trim();
28242
+ if (expr === "null") {
28243
+ out2.push({
28244
+ pattern: "csrfTokenRepository(null)",
28245
+ api: "HttpSecurity.csrfTokenRepository"
28246
+ });
28247
+ }
28248
+ }
28249
+ return out2;
28250
+ }
28251
+ fixFor(language) {
28252
+ if (language === "java") {
28253
+ return 'Leave Spring Security CSRF protection enabled. If you need to exempt a specific endpoint (e.g. webhook), use `.csrf(c -> c.ignoringRequestMatchers("/webhook"))` rather than `.disable()`. For stateless APIs, prefer a per-request token over disabling CSRF entirely.';
28254
+ }
28255
+ if (language === "python") {
28256
+ return "Remove `@csrf_exempt`. For stateless API endpoints, use Django REST Framework with a token / session auth backend that does not rely on cookies. For webhook receivers, verify a shared-secret signature instead of disabling CSRF.";
28257
+ }
28258
+ return "Re-enable framework CSRF protection or replace with origin / token validation.";
28259
+ }
28260
+ };
28261
+
28262
+ // src/analysis/passes/xml-entity-expansion-pass.ts
28263
+ var JAVA_FACTORIES = /* @__PURE__ */ new Set([
28264
+ "SAXParserFactory",
28265
+ "DocumentBuilderFactory",
28266
+ "XMLInputFactory",
28267
+ "SchemaFactory",
28268
+ "TransformerFactory"
28269
+ ]);
28270
+ var JAVA_SAFE_EVIDENCE_RE = /(disallow-doctype-decl|external-general-entities|external-parameter-entities|SUPPORT_DTD|ACCESS_EXTERNAL_DTD|ACCESS_EXTERNAL_SCHEMA|setXIncludeAware\s*\(\s*false\s*\)|setExpandEntityReferences\s*\(\s*false\s*\))/;
28271
+ var PY_LXML_PARSER_INSECURE_DEFAULT_RE = /\bresolve_entities\s*=\s*False\b/;
28272
+ var XmlEntityExpansionPass = class {
28273
+ name = "xml-entity-expansion";
28274
+ category = "security";
28275
+ run(ctx) {
28276
+ const { graph, language } = ctx;
28277
+ const file = graph.ir.meta.file;
28278
+ const findings = [];
28279
+ const code = ctx.code ?? "";
28280
+ if (language === "java") {
28281
+ const safeInFile = JAVA_SAFE_EVIDENCE_RE.test(code);
28282
+ if (safeInFile) return { findings };
28283
+ for (const call of graph.ir.calls) {
28284
+ const det = this.detectJavaCall(call);
28285
+ if (!det) continue;
28286
+ const line = call.location.line;
28287
+ findings.push({ line, language, ...det });
28288
+ ctx.addFinding({
28289
+ id: `${this.name}-${file}-${line}-${det.api}`,
28290
+ pass: this.name,
28291
+ category: this.category,
28292
+ rule_id: this.name,
28293
+ cwe: det.cwe,
28294
+ severity: "high",
28295
+ level: "error",
28296
+ message: `${det.api} created without disabling DTD / external-entity processing. Vulnerable to billion-laughs / quadratic blow-up DoS (CWE-776) and external-entity disclosure (CWE-611). Add \`setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)\` (or the equivalent) before parsing.`,
28297
+ file,
28298
+ line,
28299
+ fix: this.fixForJava(det.api),
28300
+ evidence: { ...det, language, safeFeatureInFile: false }
28301
+ });
28302
+ }
28303
+ return { findings };
28304
+ }
28305
+ if (language === "python") {
28306
+ const safeInFile = PY_LXML_PARSER_INSECURE_DEFAULT_RE.test(code) || /\bdefusedxml\b/.test(code);
28307
+ if (safeInFile) return { findings };
28308
+ for (const call of graph.ir.calls) {
28309
+ const det = this.detectPythonCall(call);
28310
+ if (!det) continue;
28311
+ const line = call.location.line;
28312
+ findings.push({ line, language, ...det });
28313
+ ctx.addFinding({
28314
+ id: `${this.name}-${file}-${line}-${det.api}`,
28315
+ pass: this.name,
28316
+ category: this.category,
28317
+ rule_id: this.name,
28318
+ cwe: det.cwe,
28319
+ severity: "high",
28320
+ level: "error",
28321
+ message: `${det.api} called without an entity-safe parser. Vulnerable to billion-laughs / quadratic blow-up DoS (CWE-776) and external-entity disclosure (CWE-611). Use \`defusedxml\` or pass an \`XMLParser(resolve_entities=False)\` to lxml.`,
28322
+ file,
28323
+ line,
28324
+ fix: this.fixForPython(det.api),
28325
+ evidence: { ...det, language, safeFeatureInFile: false }
28326
+ });
28327
+ }
28328
+ return { findings };
28329
+ }
28330
+ return { findings };
28331
+ }
28332
+ detectJavaCall(call) {
28333
+ if (call.method_name !== "newInstance") return null;
28334
+ const recv = call.receiver ?? "";
28335
+ const recvType = call.receiver_type ?? "";
28336
+ for (const factory of JAVA_FACTORIES) {
28337
+ if (recv === factory || recvType === factory || recv.endsWith("." + factory) || recvType.endsWith("." + factory)) {
28338
+ return {
28339
+ pattern: `${factory}.newInstance()`,
28340
+ api: factory,
28341
+ cwe: "CWE-776"
28342
+ };
28343
+ }
28344
+ }
28345
+ return null;
28346
+ }
28347
+ detectPythonCall(call) {
28348
+ const recv = call.receiver ?? "";
28349
+ const method = call.method_name;
28350
+ if ((method === "parse" || method === "fromstring" || method === "XML") && (recv === "etree" || recv.endsWith(".etree"))) {
28351
+ return {
28352
+ pattern: `etree.${method}`,
28353
+ api: `lxml.etree.${method}`,
28354
+ cwe: "CWE-776"
28355
+ };
28356
+ }
28357
+ if ((method === "parse" || method === "fromstring") && (recv === "ET" || recv === "ElementTree" || recv.endsWith(".ElementTree"))) {
28358
+ return {
28359
+ pattern: `ElementTree.${method}`,
28360
+ api: `xml.etree.ElementTree.${method}`,
28361
+ cwe: "CWE-776"
28362
+ };
28363
+ }
28364
+ return null;
28365
+ }
28366
+ fixForJava(api) {
28367
+ if (api === "SAXParserFactory") {
28368
+ return 'Call `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` and `factory.setXIncludeAware(false)` before `newSAXParser()`.';
28369
+ }
28370
+ if (api === "DocumentBuilderFactory") {
28371
+ return 'Call `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` and `factory.setExpandEntityReferences(false)` before `newDocumentBuilder()`.';
28372
+ }
28373
+ if (api === "XMLInputFactory") {
28374
+ return "Call `factory.setProperty(XMLInputFactory.SUPPORT_DTD, false)` and `factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false)` before `createXMLStreamReader`.";
28375
+ }
28376
+ return "Use `XMLConstants.FEATURE_SECURE_PROCESSING` and explicitly disable DTD / external-entity loading on the factory before parsing.";
28377
+ }
28378
+ fixForPython(api) {
28379
+ if (api.startsWith("lxml.etree")) {
28380
+ return "Pass an explicit parser: `etree.parse(src, parser=etree.XMLParser(resolve_entities=False, no_network=True))`. Even better, use the `defusedxml.lxml` wrapper.";
28381
+ }
28382
+ return "Replace `xml.etree.ElementTree` with `defusedxml.ElementTree`, which disables DTD / entity processing by default.";
28383
+ }
28384
+ };
28385
+
28386
+ // src/analysis/passes/mass-assignment-pass.ts
28387
+ var PY_KWARGS_SPLAT_RE = /\*\*\s*(?:request|self\.request|flask\.request|ctx|self)\s*\.\s*(?:form|args|values|json|get_json\s*\(\s*\)|files|data)/;
28388
+ var JS_OBJECT_SPREAD_RE = /\{\s*\.\.\.\s*(?:req|request|ctx|context)(?:\.request)?\s*\.\s*(?:body|query|params|form)\b/;
28389
+ var MassAssignmentPass = class {
28390
+ name = "mass-assignment";
28391
+ category = "security";
28392
+ run(ctx) {
28393
+ const { graph, language } = ctx;
28394
+ const file = graph.ir.meta.file;
28395
+ const findings = [];
28396
+ const code = ctx.code ?? "";
28397
+ if (!code) return { findings };
28398
+ const lines = code.split("\n");
28399
+ if (language === "python") {
28400
+ for (let i2 = 0; i2 < lines.length; i2++) {
28401
+ const text = lines[i2] ?? "";
28402
+ const m = PY_KWARGS_SPLAT_RE.exec(text);
28403
+ if (!m) continue;
28404
+ const line = i2 + 1;
28405
+ const det = {
28406
+ pattern: "**request.<bag>",
28407
+ match: m[0]
28408
+ };
28409
+ findings.push({
28410
+ line,
28411
+ language,
28412
+ pattern: det.pattern,
28413
+ snippet: text.trim().slice(0, 200)
28414
+ });
28415
+ ctx.addFinding({
28416
+ id: `${this.name}-${file}-${line}`,
28417
+ pass: this.name,
28418
+ category: this.category,
28419
+ rule_id: this.name,
28420
+ cwe: "CWE-915",
28421
+ severity: "high",
28422
+ level: "error",
28423
+ message: `HTTP request bag splatted into constructor / ORM helper via \`${det.match}\`. Every form field becomes a settable attribute on the domain object, including ones the endpoint did not intend to expose (e.g. \`is_admin\`, \`role\`, \`owner_id\`).`,
28424
+ file,
28425
+ line,
28426
+ fix: "Replace the `**` splat with an explicit allow-list: `Model(name=request.form['name'], email=request.form['email'])`. For Django, use a `ModelForm` / serializer with `fields = [...]`.",
28427
+ evidence: { pattern: det.pattern, match: det.match, language }
28428
+ });
28429
+ }
28430
+ return { findings };
28431
+ }
28432
+ if (language === "javascript" || language === "typescript") {
28433
+ for (let i2 = 0; i2 < lines.length; i2++) {
28434
+ const text = lines[i2] ?? "";
28435
+ const m = JS_OBJECT_SPREAD_RE.exec(text);
28436
+ if (!m) continue;
28437
+ const line = i2 + 1;
28438
+ findings.push({
28439
+ line,
28440
+ language,
28441
+ pattern: "{...req.<bag>}",
28442
+ snippet: text.trim().slice(0, 200)
28443
+ });
28444
+ ctx.addFinding({
28445
+ id: `${this.name}-${file}-${line}`,
28446
+ pass: this.name,
28447
+ category: this.category,
28448
+ rule_id: this.name,
28449
+ cwe: "CWE-915",
28450
+ severity: "high",
28451
+ level: "error",
28452
+ message: `HTTP request bag spread into object literal via \`${m[0]}\`. Every body field becomes a settable property on the resulting object, including ones the endpoint did not intend to expose (e.g. \`isAdmin\`, \`role\`, \`ownerId\`).`,
28453
+ file,
28454
+ line,
28455
+ fix: "Replace the spread with an explicit pick: `const { name, email } = req.body; const user = { name, email };`. For ORMs, use a DTO / Zod schema with `.pick(...)` or allow-list serializers.",
28456
+ evidence: { pattern: "{...req.<bag>}", match: m[0], language }
28457
+ });
28458
+ }
28459
+ return { findings };
28460
+ }
28461
+ return { findings };
28462
+ }
28463
+ };
28464
+
28090
28465
  // src/analysis/metrics/passes/size-metrics-pass.ts
28091
28466
  var SizeMetricsPass = class {
28092
28467
  name = "size-metrics";
@@ -28989,6 +29364,9 @@ async function analyze(code, filePath, language, options = {}) {
28989
29364
  if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
28990
29365
  if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
28991
29366
  if (!disabledPasses.has("jwt-verify-disabled")) pipeline.add(new JwtVerifyDisabledPass());
29367
+ if (!disabledPasses.has("csrf-protection-disabled")) pipeline.add(new CsrfProtectionDisabledPass());
29368
+ if (!disabledPasses.has("xml-entity-expansion")) pipeline.add(new XmlEntityExpansionPass());
29369
+ if (!disabledPasses.has("mass-assignment")) pipeline.add(new MassAssignmentPass());
28992
29370
  const { results, findings } = pipeline.run(graph, code, language, config);
28993
29371
  const sinkFilter = results.get("sink-filter");
28994
29372
  const interProc = results.get("interprocedural");
@@ -10559,9 +10559,16 @@ var DEFAULT_SINKS = [
10559
10559
  { method: "println", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
10560
10560
  // XSS in error messages (CWE-81)
10561
10561
  { method: "sendError", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10562
- // Response header injection (can lead to header XSS)
10563
- { method: "setHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10564
- { method: "addHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10562
+ // Response header injection re-categorised from `xss` to `crlf`
10563
+ // (CWE-113) in Sprint 6 of #86. Header injection is HTTP response
10564
+ // splitting / cache-poisoning / cookie forging; reflected XSS via header
10565
+ // reflection remains a downstream concern of body-writing sinks.
10566
+ { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10567
+ { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10568
+ // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10569
+ // (CWE-601) further down — see entry near line 1195. CRLF via Location
10570
+ // header is a secondary concern; keeping the canonical SSRF entry avoids
10571
+ // double-emission that would mask the open-redirect chain.
10565
10572
  { method: "setContentType", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
10566
10573
  // JSP output
10567
10574
  { method: "setAttribute", class: "PageContext", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
@@ -11530,7 +11537,33 @@ var DEFAULT_SINKS = [
11530
11537
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11531
11538
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11532
11539
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11533
- { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
11540
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
11541
+ // CRLF / HTTP response splitting (CWE-113) — Sprint 6, #86.
11542
+ // Node.js / Express response header / cookie sinks. The header *name* (arg 0)
11543
+ // is also CRLF-sensitive but is almost always a string literal; we model
11544
+ // arg 1 (the value) as the primary sink.
11545
+ { method: "setHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11546
+ { method: "writeHead", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [2], languages: ["javascript", "typescript"] },
11547
+ // Express: res.cookie(name, value, options) — value is CRLF-sensitive.
11548
+ { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11549
+ // Express: res.location(url) and res.redirect(url) — Location header.
11550
+ { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11551
+ { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11552
+ // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
11553
+ // (Header is a map; the actual `value` is arg 1 of the call). We flag the
11554
+ // value position so a tainted variable is detected.
11555
+ { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11556
+ { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11557
+ // Mass-assignment (CWE-915) — Sprint 6, #86.
11558
+ // JS Object.assign(target, ...sources) — sources are arg 1..N, and if any
11559
+ // source is request-tainted, every key gets written onto the target. We
11560
+ // flag the source positions; the analyzer only needs one tainted to fire.
11561
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11562
+ // Lodash bulk-merge helpers behave identically.
11563
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11564
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11565
+ // jQuery $.extend(target, source) (legacy).
11566
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
11534
11567
  ];
11535
11568
  var DEFAULT_SANITIZERS = [
11536
11569
  // SQL Injection - proper parameter binding sanitizes input
@@ -13186,7 +13219,9 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
13186
13219
  "code_injection",
13187
13220
  "mybatis_mapper_call",
13188
13221
  "redos",
13189
- "format_string"
13222
+ "format_string",
13223
+ "crlf",
13224
+ "mass_assignment"
13190
13225
  ]);
13191
13226
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
13192
13227
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -10493,9 +10493,16 @@ var DEFAULT_SINKS = [
10493
10493
  { method: "println", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
10494
10494
  // XSS in error messages (CWE-81)
10495
10495
  { method: "sendError", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10496
- // Response header injection (can lead to header XSS)
10497
- { method: "setHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10498
- { method: "addHeader", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
10496
+ // Response header injection re-categorised from `xss` to `crlf`
10497
+ // (CWE-113) in Sprint 6 of #86. Header injection is HTTP response
10498
+ // splitting / cache-poisoning / cookie forging; reflected XSS via header
10499
+ // reflection remains a downstream concern of body-writing sinks.
10500
+ { method: "setHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10501
+ { method: "addHeader", class: "HttpServletResponse", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1] },
10502
+ // Note: `sendRedirect` is primarily classified as `ssrf` / open-redirect
10503
+ // (CWE-601) further down — see entry near line 1195. CRLF via Location
10504
+ // header is a secondary concern; keeping the canonical SSRF entry avoids
10505
+ // double-emission that would mask the open-redirect chain.
10499
10506
  { method: "setContentType", class: "HttpServletResponse", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
10500
10507
  // JSP output
10501
10508
  { method: "setAttribute", class: "PageContext", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [1] },
@@ -11464,7 +11471,33 @@ var DEFAULT_SINKS = [
11464
11471
  { method: "Sprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11465
11472
  { method: "Printf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11466
11473
  { method: "Errorf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["go"] },
11467
- { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] }
11474
+ { method: "Fprintf", class: "fmt", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [1], languages: ["go"] },
11475
+ // CRLF / HTTP response splitting (CWE-113) — Sprint 6, #86.
11476
+ // Node.js / Express response header / cookie sinks. The header *name* (arg 0)
11477
+ // is also CRLF-sensitive but is almost always a string literal; we model
11478
+ // arg 1 (the value) as the primary sink.
11479
+ { method: "setHeader", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11480
+ { method: "writeHead", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [2], languages: ["javascript", "typescript"] },
11481
+ // Express: res.cookie(name, value, options) — value is CRLF-sensitive.
11482
+ { method: "cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["javascript", "typescript"] },
11483
+ // Express: res.location(url) and res.redirect(url) — Location header.
11484
+ { method: "location", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11485
+ { method: "redirect", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11486
+ // Go net/http: w.Header().Set(k, v) / Add(k, v) — first arg is the value
11487
+ // (Header is a map; the actual `value` is arg 1 of the call). We flag the
11488
+ // value position so a tainted variable is detected.
11489
+ { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11490
+ { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11491
+ // Mass-assignment (CWE-915) — Sprint 6, #86.
11492
+ // JS Object.assign(target, ...sources) — sources are arg 1..N, and if any
11493
+ // source is request-tainted, every key gets written onto the target. We
11494
+ // flag the source positions; the analyzer only needs one tainted to fire.
11495
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11496
+ // Lodash bulk-merge helpers behave identically.
11497
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11498
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11499
+ // jQuery $.extend(target, source) (legacy).
11500
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
11468
11501
  ];
11469
11502
  var DEFAULT_SANITIZERS = [
11470
11503
  // SQL Injection - proper parameter binding sanitizes input
@@ -13120,7 +13153,9 @@ var KNOWN_SINK_TYPES = /* @__PURE__ */ new Set([
13120
13153
  "code_injection",
13121
13154
  "mybatis_mapper_call",
13122
13155
  "redos",
13123
- "format_string"
13156
+ "format_string",
13157
+ "crlf",
13158
+ "mass_assignment"
13124
13159
  ]);
13125
13160
  function checkSanitized(_fromLine, toLine, sinkType, sanitizersByLine) {
13126
13161
  const sanitizersAtTarget = sanitizersByLine.get(toLine);
@@ -158,7 +158,7 @@ export interface TaintFlowStep {
158
158
  type: 'source' | 'assignment' | 'use' | 'return' | 'field' | 'sink';
159
159
  }
160
160
  export type SourceType = "http_param" | "http_body" | "http_header" | "http_cookie" | "http_path" | "http_query" | "io_input" | "env_input" | "db_input" | "network_input" | "file_input" | "dom_input" | "config_param" | "interprocedural_param" | "plugin_param" | "constructor_field";
161
- export type SinkType = "sql_injection" | "nosql_injection" | "command_injection" | "path_traversal" | "xss" | "xxe" | "deserialization" | "ldap_injection" | "xpath_injection" | "ssrf" | "open_redirect" | "code_injection" | "log_injection" | "redos" | "format_string" | "mybatis_mapper_call" | "weak_random" | "weak_hash" | "weak_crypto" | "insecure_cookie" | "trust_boundary" | "external_taint_escape";
161
+ export type SinkType = "sql_injection" | "nosql_injection" | "command_injection" | "path_traversal" | "xss" | "xxe" | "deserialization" | "ldap_injection" | "xpath_injection" | "ssrf" | "open_redirect" | "code_injection" | "log_injection" | "redos" | "format_string" | "crlf" | "mass_assignment" | "mybatis_mapper_call" | "weak_random" | "weak_hash" | "weak_crypto" | "insecure_cookie" | "trust_boundary" | "external_taint_escape";
162
162
  export type Severity = "critical" | "high" | "medium" | "low";
163
163
  export interface TaintSource {
164
164
  type: SourceType;