circle-ir 3.54.0 → 3.56.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 (36) 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/weak-crypto-pass.d.ts +10 -0
  16. package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/weak-crypto-pass.js +263 -3
  18. package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
  19. package/dist/analysis/passes/xml-entity-expansion-pass.d.ts +58 -0
  20. package/dist/analysis/passes/xml-entity-expansion-pass.d.ts.map +1 -0
  21. package/dist/analysis/passes/xml-entity-expansion-pass.js +196 -0
  22. package/dist/analysis/passes/xml-entity-expansion-pass.js.map +1 -0
  23. package/dist/analysis/rules.d.ts.map +1 -1
  24. package/dist/analysis/rules.js +18 -0
  25. package/dist/analysis/rules.js.map +1 -1
  26. package/dist/analysis/taint-propagation.js +1 -1
  27. package/dist/analysis/taint-propagation.js.map +1 -1
  28. package/dist/analyzer.d.ts.map +1 -1
  29. package/dist/analyzer.js +9 -0
  30. package/dist/analyzer.js.map +1 -1
  31. package/dist/browser/circle-ir.js +533 -14
  32. package/dist/core/circle-ir-core.cjs +40 -5
  33. package/dist/core/circle-ir-core.js +40 -5
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/index.d.ts.map +1 -1
  36. 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);
@@ -27445,6 +27485,102 @@ function detectHardcodedKeyJava(call) {
27445
27485
  if (/^"[^"]*"$/.test(expr)) return `literal string`;
27446
27486
  return null;
27447
27487
  }
27488
+ function detectHardcodedKeyPython(call, constProp, literalBindings) {
27489
+ const arg = call.arguments.find((a) => a.position === 0);
27490
+ if (!arg) return null;
27491
+ const expr = (arg.expression ?? arg.literal ?? "").trim();
27492
+ if (!expr) return null;
27493
+ if (/^[bB][rR]?["'][^"']*["']$/.test(expr) || /^[rR][bB]["'][^"']*["']$/.test(expr)) {
27494
+ return `literal bytes ${expr.slice(0, 24)}${expr.length > 24 ? "\u2026" : ""}`;
27495
+ }
27496
+ if (/^["'][^"']*["']$/.test(expr)) {
27497
+ return `literal string ${expr.slice(0, 24)}${expr.length > 24 ? "\u2026" : ""}`;
27498
+ }
27499
+ if (arg.variable && constProp) {
27500
+ const sym = constProp.symbols.get(arg.variable);
27501
+ if (sym && sym.type === "string" && typeof sym.value === "string") {
27502
+ return `constant-propagated bytes from \`${arg.variable}\``;
27503
+ }
27504
+ }
27505
+ if (arg.variable) {
27506
+ const lit = literalBindings.get(arg.variable);
27507
+ if (lit) {
27508
+ return `literal-bound ${arg.variable} = ${lit.slice(0, 24)}${lit.length > 24 ? "\u2026" : ""}`;
27509
+ }
27510
+ }
27511
+ return null;
27512
+ }
27513
+ function detectHardcodedKeyGo(call, constProp, literalBindings) {
27514
+ const arg = call.arguments.find((a) => a.position === 0);
27515
+ if (!arg) return null;
27516
+ const expr = (arg.literal ?? arg.expression ?? "").trim();
27517
+ if (!expr) return null;
27518
+ if (/^\[\s*\]\s*byte\s*\(\s*["'`][^"'`]*["'`]\s*\)$/.test(expr)) {
27519
+ return `literal []byte("\u2026")`;
27520
+ }
27521
+ if (/^\[\s*\]\s*byte\s*\{[^}]*\}$/.test(expr)) {
27522
+ return `literal []byte{\u2026} composite`;
27523
+ }
27524
+ if (arg.variable && constProp) {
27525
+ const sym = constProp.symbols.get(arg.variable);
27526
+ if (sym && sym.type === "string" && typeof sym.value === "string") {
27527
+ return `constant-propagated key from \`${arg.variable}\``;
27528
+ }
27529
+ }
27530
+ if (arg.variable) {
27531
+ const lit = literalBindings.get(arg.variable);
27532
+ if (lit) {
27533
+ return `literal-bound ${arg.variable} = ${lit.slice(0, 24)}${lit.length > 24 ? "\u2026" : ""}`;
27534
+ }
27535
+ }
27536
+ return null;
27537
+ }
27538
+ function parseWeakRsaKeySizePython(call) {
27539
+ for (const arg of call.arguments) {
27540
+ const expr = (arg.expression ?? "").trim();
27541
+ const lit = (arg.literal ?? "").trim();
27542
+ const m = expr.match(/^key_size\s*=\s*(-?\d+)\s*$/);
27543
+ if (m && m[1]) {
27544
+ const n = parseInt(m[1], 10);
27545
+ if (Number.isFinite(n) && n > 0 && n < 2048) return n;
27546
+ return null;
27547
+ }
27548
+ if (/^key_size\s*=/.test(expr) && lit) {
27549
+ const n = parseInt(lit, 10);
27550
+ if (Number.isFinite(n) && n > 0 && n < 2048) return n;
27551
+ }
27552
+ }
27553
+ return null;
27554
+ }
27555
+ function scanLiteralBindings(code, language) {
27556
+ const out2 = /* @__PURE__ */ new Map();
27557
+ if (!code) return out2;
27558
+ if (language === "python") {
27559
+ const re = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(b[rR]?["'][^"']*["']|[rR]?b["'][^"']*["']|["'][^"']*["'])\s*(?:$|#)/gm;
27560
+ let m;
27561
+ while ((m = re.exec(code)) !== null) {
27562
+ if (m[1] && m[2]) out2.set(m[1], m[2]);
27563
+ }
27564
+ return out2;
27565
+ }
27566
+ if (language === "go") {
27567
+ const reByte = /^[ \t]*(?:var\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*(?::=|=)\s*(\[\s*\]\s*byte\s*\(\s*["'`][^"'`]*["'`]\s*\))/gm;
27568
+ let m;
27569
+ while ((m = reByte.exec(code)) !== null) {
27570
+ if (m[1] && m[2]) out2.set(m[1], m[2]);
27571
+ }
27572
+ const reStr = /^[ \t]*(?:var|const)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(["'`][^"'`]*["'`])/gm;
27573
+ while ((m = reStr.exec(code)) !== null) {
27574
+ if (m[1] && m[2]) out2.set(m[1], m[2]);
27575
+ }
27576
+ const reShort = /^[ \t]*([A-Za-z_][A-Za-z0-9_]*)\s*:=\s*(["'`][^"'`]*["'`])/gm;
27577
+ while ((m = reShort.exec(code)) !== null) {
27578
+ if (m[1] && m[2]) out2.set(m[1], m[2]);
27579
+ }
27580
+ return out2;
27581
+ }
27582
+ return out2;
27583
+ }
27448
27584
  var ISSUE_CWE = {
27449
27585
  "weak-cipher": "CWE-327",
27450
27586
  "ecb-mode": "CWE-327",
@@ -27457,11 +27593,13 @@ var WeakCryptoPass = class {
27457
27593
  name = "weak-crypto";
27458
27594
  category = "security";
27459
27595
  run(ctx) {
27460
- const { graph, language } = ctx;
27596
+ const { graph, language, code } = ctx;
27461
27597
  const file = graph.ir.meta.file;
27462
27598
  const findings = [];
27599
+ const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
27600
+ const literalBindings = scanLiteralBindings(code, language);
27463
27601
  for (const call of graph.ir.calls) {
27464
- const detections = this.detect(call, language);
27602
+ const detections = this.detect(call, language, constProp, literalBindings);
27465
27603
  for (const det of detections) {
27466
27604
  const line = call.location.line;
27467
27605
  findings.push({ line, language, ...det });
@@ -27514,7 +27652,7 @@ var WeakCryptoPass = class {
27514
27652
  return "Use AES-GCM (authenticated) or ChaCha20-Poly1305. Avoid DES, 3DES, RC2, RC4, Blowfish, and ECB mode. For asymmetric encryption use RSA-OAEP with \u22652048-bit keys or modern curve-based schemes.";
27515
27653
  }
27516
27654
  }
27517
- detect(call, language) {
27655
+ detect(call, language, constProp, literalBindings) {
27518
27656
  const method = call.method_name;
27519
27657
  const receiver = call.receiver ?? "";
27520
27658
  const out2 = [];
@@ -27572,6 +27710,12 @@ var WeakCryptoPass = class {
27572
27710
  out2.push({ issue: "ecb-mode", detail: "AES.MODE_ECB", api: `${receiver}.new` });
27573
27711
  }
27574
27712
  }
27713
+ if (lastSeg === "aes" || lastSeg.endsWith(".aes") || WEAK_CIPHER_BASES.has(lastSeg)) {
27714
+ const keyDetail = detectHardcodedKeyPython(call, constProp, literalBindings);
27715
+ if (keyDetail) {
27716
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `${receiver}.new` });
27717
+ }
27718
+ }
27575
27719
  }
27576
27720
  const isHazmatAlgos = receiver === "algorithms" || receiver.endsWith(".algorithms");
27577
27721
  if (isHazmatAlgos) {
@@ -27580,6 +27724,25 @@ var WeakCryptoPass = class {
27580
27724
  if (WEAK_CIPHER_BASES.has(normalized)) {
27581
27725
  out2.push({ issue: "weak-cipher", detail: normalized, api: `algorithms.${method}` });
27582
27726
  }
27727
+ if (m === "aes") {
27728
+ const keyDetail = detectHardcodedKeyPython(call, constProp, literalBindings);
27729
+ if (keyDetail) {
27730
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `algorithms.${method}` });
27731
+ }
27732
+ }
27733
+ }
27734
+ if (method === "ECB" && (receiver === "modes" || receiver.endsWith(".modes"))) {
27735
+ out2.push({ issue: "ecb-mode", detail: "modes.ECB()", api: `${receiver}.ECB` });
27736
+ }
27737
+ if (method === "generate_private_key" && (receiver === "rsa" || receiver === "dsa" || receiver.endsWith(".rsa") || receiver.endsWith(".dsa"))) {
27738
+ const n = parseWeakRsaKeySizePython(call);
27739
+ if (n !== null) {
27740
+ out2.push({
27741
+ issue: "weak-rsa-key",
27742
+ detail: String(n),
27743
+ api: `${receiver}.generate_private_key`
27744
+ });
27745
+ }
27583
27746
  }
27584
27747
  return out2;
27585
27748
  }
@@ -27619,6 +27782,24 @@ var WeakCryptoPass = class {
27619
27782
  if ((method === "NewECBEncrypter" || method === "NewECBDecrypter") && receiver === "cipher") {
27620
27783
  out2.push({ issue: "ecb-mode", detail: method, api: `cipher.${method}` });
27621
27784
  }
27785
+ if (receiver === "aes" && method === "NewCipher" || receiver === "des" && (method === "NewCipher" || method === "NewTripleDESCipher") || receiver === "rc4" && method === "NewCipher") {
27786
+ const keyDetail = detectHardcodedKeyGo(call, constProp, literalBindings);
27787
+ if (keyDetail) {
27788
+ out2.push({ issue: "hardcoded-key", detail: keyDetail, api: `${receiver}.${method}` });
27789
+ }
27790
+ }
27791
+ if (receiver === "rsa" && method === "GenerateKey") {
27792
+ const bitsArg = call.arguments.find((a) => a.position === 1);
27793
+ const expr = (bitsArg?.literal ?? bitsArg?.expression ?? "").trim();
27794
+ const n = parseInt(expr, 10);
27795
+ if (Number.isFinite(n) && n > 0 && n < 2048) {
27796
+ out2.push({
27797
+ issue: "weak-rsa-key",
27798
+ detail: String(n),
27799
+ api: "rsa.GenerateKey"
27800
+ });
27801
+ }
27802
+ }
27622
27803
  return out2;
27623
27804
  }
27624
27805
  return out2;
@@ -28087,6 +28268,341 @@ var JwtVerifyDisabledPass = class {
28087
28268
  }
28088
28269
  };
28089
28270
 
28271
+ // src/analysis/passes/csrf-protection-disabled-pass.ts
28272
+ var JAVA_CSRF_DISABLE_RE = /\.csrf\s*\([^)]*\)\s*\.\s*disable\b/;
28273
+ var JAVA_CSRF_LAMBDA_DISABLE_RE = /\bcsrf\s*\(\s*\w+\s*->\s*\w+\s*\.\s*disable\s*\(/;
28274
+ var JAVA_CSRF_METHODREF_RE = /\bcsrf\s*\(\s*[\w.]+::disable\s*\)/;
28275
+ var JAVA_CSRF_NULL_REPO_RE = /\.csrfTokenRepository\s*\(\s*null\s*\)/;
28276
+ var CsrfProtectionDisabledPass = class {
28277
+ name = "csrf-protection-disabled";
28278
+ category = "security";
28279
+ run(ctx) {
28280
+ const { graph, language } = ctx;
28281
+ const file = graph.ir.meta.file;
28282
+ const findings = [];
28283
+ for (const call of graph.ir.calls) {
28284
+ const detections = this.detectCall(call, language);
28285
+ for (const det of detections) {
28286
+ const line = call.location.line;
28287
+ findings.push({ line, language, ...det });
28288
+ ctx.addFinding({
28289
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28290
+ pass: this.name,
28291
+ category: this.category,
28292
+ rule_id: this.name,
28293
+ cwe: "CWE-352",
28294
+ severity: "critical",
28295
+ level: "error",
28296
+ 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.`,
28297
+ file,
28298
+ line,
28299
+ fix: this.fixFor(language),
28300
+ evidence: { ...det, language }
28301
+ });
28302
+ }
28303
+ }
28304
+ if (language === "java") {
28305
+ const src = ctx.code ?? "";
28306
+ if (src) {
28307
+ const lines = src.split("\n");
28308
+ for (let i2 = 0; i2 < lines.length; i2++) {
28309
+ const line = i2 + 1;
28310
+ const text = lines[i2] ?? "";
28311
+ let det = null;
28312
+ if (JAVA_CSRF_LAMBDA_DISABLE_RE.test(text)) {
28313
+ det = { pattern: "csrf(c -> c.disable())", api: "HttpSecurity.csrf" };
28314
+ } else if (JAVA_CSRF_METHODREF_RE.test(text)) {
28315
+ det = { pattern: "csrf(::disable)", api: "HttpSecurity.csrf" };
28316
+ } else if (JAVA_CSRF_NULL_REPO_RE.test(text)) {
28317
+ det = { pattern: "csrfTokenRepository(null)", api: "HttpSecurity.csrfTokenRepository" };
28318
+ } else if (JAVA_CSRF_DISABLE_RE.test(text)) {
28319
+ det = { pattern: "csrf().disable()", api: "HttpSecurity.csrf" };
28320
+ }
28321
+ if (det && !findings.some((f) => f.line === line && f.pattern === det.pattern)) {
28322
+ findings.push({ line, language, ...det });
28323
+ ctx.addFinding({
28324
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28325
+ pass: this.name,
28326
+ category: this.category,
28327
+ rule_id: this.name,
28328
+ cwe: "CWE-352",
28329
+ severity: "critical",
28330
+ level: "error",
28331
+ 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.`,
28332
+ file,
28333
+ line,
28334
+ fix: this.fixFor(language),
28335
+ evidence: { ...det, language }
28336
+ });
28337
+ }
28338
+ }
28339
+ }
28340
+ }
28341
+ if (language === "python") {
28342
+ const src = ctx.code ?? "";
28343
+ if (src) {
28344
+ const lines = src.split("\n");
28345
+ for (let i2 = 0; i2 < lines.length; i2++) {
28346
+ const text = lines[i2] ?? "";
28347
+ if (/^\s*@csrf_exempt\b/.test(text)) {
28348
+ const line = i2 + 1;
28349
+ const det = { pattern: "@csrf_exempt", api: "django.views.decorators.csrf" };
28350
+ findings.push({ line, language, ...det });
28351
+ ctx.addFinding({
28352
+ id: `${this.name}-${file}-${line}-${det.pattern}`,
28353
+ pass: this.name,
28354
+ category: this.category,
28355
+ rule_id: this.name,
28356
+ cwe: "CWE-352",
28357
+ severity: "critical",
28358
+ level: "error",
28359
+ 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.",
28360
+ file,
28361
+ line,
28362
+ fix: this.fixFor(language),
28363
+ evidence: { ...det, language }
28364
+ });
28365
+ }
28366
+ }
28367
+ }
28368
+ }
28369
+ return { findings };
28370
+ }
28371
+ detectCall(call, language) {
28372
+ const out2 = [];
28373
+ if (language !== "java") return out2;
28374
+ if (call.method_name === "disable") {
28375
+ const recv = call.receiver ?? "";
28376
+ if (/\bcsrf\s*\(\s*\)\s*$/.test(recv) || recv.endsWith(".csrf()")) {
28377
+ out2.push({ pattern: "csrf().disable()", api: "HttpSecurity.csrf" });
28378
+ }
28379
+ }
28380
+ if (call.method_name === "csrfTokenRepository") {
28381
+ const arg = call.arguments.find((a) => a.position === 0);
28382
+ const expr = (arg?.expression ?? arg?.literal ?? "").trim();
28383
+ if (expr === "null") {
28384
+ out2.push({
28385
+ pattern: "csrfTokenRepository(null)",
28386
+ api: "HttpSecurity.csrfTokenRepository"
28387
+ });
28388
+ }
28389
+ }
28390
+ return out2;
28391
+ }
28392
+ fixFor(language) {
28393
+ if (language === "java") {
28394
+ 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.';
28395
+ }
28396
+ if (language === "python") {
28397
+ 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.";
28398
+ }
28399
+ return "Re-enable framework CSRF protection or replace with origin / token validation.";
28400
+ }
28401
+ };
28402
+
28403
+ // src/analysis/passes/xml-entity-expansion-pass.ts
28404
+ var JAVA_FACTORIES = /* @__PURE__ */ new Set([
28405
+ "SAXParserFactory",
28406
+ "DocumentBuilderFactory",
28407
+ "XMLInputFactory",
28408
+ "SchemaFactory",
28409
+ "TransformerFactory"
28410
+ ]);
28411
+ 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*\))/;
28412
+ var PY_LXML_PARSER_INSECURE_DEFAULT_RE = /\bresolve_entities\s*=\s*False\b/;
28413
+ var XmlEntityExpansionPass = class {
28414
+ name = "xml-entity-expansion";
28415
+ category = "security";
28416
+ run(ctx) {
28417
+ const { graph, language } = ctx;
28418
+ const file = graph.ir.meta.file;
28419
+ const findings = [];
28420
+ const code = ctx.code ?? "";
28421
+ if (language === "java") {
28422
+ const safeInFile = JAVA_SAFE_EVIDENCE_RE.test(code);
28423
+ if (safeInFile) return { findings };
28424
+ for (const call of graph.ir.calls) {
28425
+ const det = this.detectJavaCall(call);
28426
+ if (!det) continue;
28427
+ const line = call.location.line;
28428
+ findings.push({ line, language, ...det });
28429
+ ctx.addFinding({
28430
+ id: `${this.name}-${file}-${line}-${det.api}`,
28431
+ pass: this.name,
28432
+ category: this.category,
28433
+ rule_id: this.name,
28434
+ cwe: det.cwe,
28435
+ severity: "high",
28436
+ level: "error",
28437
+ 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.`,
28438
+ file,
28439
+ line,
28440
+ fix: this.fixForJava(det.api),
28441
+ evidence: { ...det, language, safeFeatureInFile: false }
28442
+ });
28443
+ }
28444
+ return { findings };
28445
+ }
28446
+ if (language === "python") {
28447
+ const safeInFile = PY_LXML_PARSER_INSECURE_DEFAULT_RE.test(code) || /\bdefusedxml\b/.test(code);
28448
+ if (safeInFile) return { findings };
28449
+ for (const call of graph.ir.calls) {
28450
+ const det = this.detectPythonCall(call);
28451
+ if (!det) continue;
28452
+ const line = call.location.line;
28453
+ findings.push({ line, language, ...det });
28454
+ ctx.addFinding({
28455
+ id: `${this.name}-${file}-${line}-${det.api}`,
28456
+ pass: this.name,
28457
+ category: this.category,
28458
+ rule_id: this.name,
28459
+ cwe: det.cwe,
28460
+ severity: "high",
28461
+ level: "error",
28462
+ 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.`,
28463
+ file,
28464
+ line,
28465
+ fix: this.fixForPython(det.api),
28466
+ evidence: { ...det, language, safeFeatureInFile: false }
28467
+ });
28468
+ }
28469
+ return { findings };
28470
+ }
28471
+ return { findings };
28472
+ }
28473
+ detectJavaCall(call) {
28474
+ if (call.method_name !== "newInstance") return null;
28475
+ const recv = call.receiver ?? "";
28476
+ const recvType = call.receiver_type ?? "";
28477
+ for (const factory of JAVA_FACTORIES) {
28478
+ if (recv === factory || recvType === factory || recv.endsWith("." + factory) || recvType.endsWith("." + factory)) {
28479
+ return {
28480
+ pattern: `${factory}.newInstance()`,
28481
+ api: factory,
28482
+ cwe: "CWE-776"
28483
+ };
28484
+ }
28485
+ }
28486
+ return null;
28487
+ }
28488
+ detectPythonCall(call) {
28489
+ const recv = call.receiver ?? "";
28490
+ const method = call.method_name;
28491
+ if ((method === "parse" || method === "fromstring" || method === "XML") && (recv === "etree" || recv.endsWith(".etree"))) {
28492
+ return {
28493
+ pattern: `etree.${method}`,
28494
+ api: `lxml.etree.${method}`,
28495
+ cwe: "CWE-776"
28496
+ };
28497
+ }
28498
+ if ((method === "parse" || method === "fromstring") && (recv === "ET" || recv === "ElementTree" || recv.endsWith(".ElementTree"))) {
28499
+ return {
28500
+ pattern: `ElementTree.${method}`,
28501
+ api: `xml.etree.ElementTree.${method}`,
28502
+ cwe: "CWE-776"
28503
+ };
28504
+ }
28505
+ return null;
28506
+ }
28507
+ fixForJava(api) {
28508
+ if (api === "SAXParserFactory") {
28509
+ return 'Call `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` and `factory.setXIncludeAware(false)` before `newSAXParser()`.';
28510
+ }
28511
+ if (api === "DocumentBuilderFactory") {
28512
+ return 'Call `factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)` and `factory.setExpandEntityReferences(false)` before `newDocumentBuilder()`.';
28513
+ }
28514
+ if (api === "XMLInputFactory") {
28515
+ return "Call `factory.setProperty(XMLInputFactory.SUPPORT_DTD, false)` and `factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false)` before `createXMLStreamReader`.";
28516
+ }
28517
+ return "Use `XMLConstants.FEATURE_SECURE_PROCESSING` and explicitly disable DTD / external-entity loading on the factory before parsing.";
28518
+ }
28519
+ fixForPython(api) {
28520
+ if (api.startsWith("lxml.etree")) {
28521
+ return "Pass an explicit parser: `etree.parse(src, parser=etree.XMLParser(resolve_entities=False, no_network=True))`. Even better, use the `defusedxml.lxml` wrapper.";
28522
+ }
28523
+ return "Replace `xml.etree.ElementTree` with `defusedxml.ElementTree`, which disables DTD / entity processing by default.";
28524
+ }
28525
+ };
28526
+
28527
+ // src/analysis/passes/mass-assignment-pass.ts
28528
+ 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)/;
28529
+ var JS_OBJECT_SPREAD_RE = /\{\s*\.\.\.\s*(?:req|request|ctx|context)(?:\.request)?\s*\.\s*(?:body|query|params|form)\b/;
28530
+ var MassAssignmentPass = class {
28531
+ name = "mass-assignment";
28532
+ category = "security";
28533
+ run(ctx) {
28534
+ const { graph, language } = ctx;
28535
+ const file = graph.ir.meta.file;
28536
+ const findings = [];
28537
+ const code = ctx.code ?? "";
28538
+ if (!code) return { findings };
28539
+ const lines = code.split("\n");
28540
+ if (language === "python") {
28541
+ for (let i2 = 0; i2 < lines.length; i2++) {
28542
+ const text = lines[i2] ?? "";
28543
+ const m = PY_KWARGS_SPLAT_RE.exec(text);
28544
+ if (!m) continue;
28545
+ const line = i2 + 1;
28546
+ const det = {
28547
+ pattern: "**request.<bag>",
28548
+ match: m[0]
28549
+ };
28550
+ findings.push({
28551
+ line,
28552
+ language,
28553
+ pattern: det.pattern,
28554
+ snippet: text.trim().slice(0, 200)
28555
+ });
28556
+ ctx.addFinding({
28557
+ id: `${this.name}-${file}-${line}`,
28558
+ pass: this.name,
28559
+ category: this.category,
28560
+ rule_id: this.name,
28561
+ cwe: "CWE-915",
28562
+ severity: "high",
28563
+ level: "error",
28564
+ 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\`).`,
28565
+ file,
28566
+ line,
28567
+ 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 = [...]`.",
28568
+ evidence: { pattern: det.pattern, match: det.match, language }
28569
+ });
28570
+ }
28571
+ return { findings };
28572
+ }
28573
+ if (language === "javascript" || language === "typescript") {
28574
+ for (let i2 = 0; i2 < lines.length; i2++) {
28575
+ const text = lines[i2] ?? "";
28576
+ const m = JS_OBJECT_SPREAD_RE.exec(text);
28577
+ if (!m) continue;
28578
+ const line = i2 + 1;
28579
+ findings.push({
28580
+ line,
28581
+ language,
28582
+ pattern: "{...req.<bag>}",
28583
+ snippet: text.trim().slice(0, 200)
28584
+ });
28585
+ ctx.addFinding({
28586
+ id: `${this.name}-${file}-${line}`,
28587
+ pass: this.name,
28588
+ category: this.category,
28589
+ rule_id: this.name,
28590
+ cwe: "CWE-915",
28591
+ severity: "high",
28592
+ level: "error",
28593
+ 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\`).`,
28594
+ file,
28595
+ line,
28596
+ 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.",
28597
+ evidence: { pattern: "{...req.<bag>}", match: m[0], language }
28598
+ });
28599
+ }
28600
+ return { findings };
28601
+ }
28602
+ return { findings };
28603
+ }
28604
+ };
28605
+
28090
28606
  // src/analysis/metrics/passes/size-metrics-pass.ts
28091
28607
  var SizeMetricsPass = class {
28092
28608
  name = "size-metrics";
@@ -28989,6 +29505,9 @@ async function analyze(code, filePath, language, options = {}) {
28989
29505
  if (!disabledPasses.has("weak-random")) pipeline.add(new WeakRandomPass());
28990
29506
  if (!disabledPasses.has("tls-verify-disabled")) pipeline.add(new TlsVerifyDisabledPass());
28991
29507
  if (!disabledPasses.has("jwt-verify-disabled")) pipeline.add(new JwtVerifyDisabledPass());
29508
+ if (!disabledPasses.has("csrf-protection-disabled")) pipeline.add(new CsrfProtectionDisabledPass());
29509
+ if (!disabledPasses.has("xml-entity-expansion")) pipeline.add(new XmlEntityExpansionPass());
29510
+ if (!disabledPasses.has("mass-assignment")) pipeline.add(new MassAssignmentPass());
28992
29511
  const { results, findings } = pipeline.run(graph, code, language, config);
28993
29512
  const sinkFilter = results.get("sink-filter");
28994
29513
  const interProc = results.get("interprocedural");