circle-ir 3.47.0 → 3.49.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 (53) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +86 -2
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  5. package/dist/analysis/constant-propagation/index.js +16 -6
  6. package/dist/analysis/constant-propagation/index.js.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.d.ts +7 -0
  8. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  9. package/dist/analysis/constant-propagation/propagator.js +81 -41
  10. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  11. package/dist/analysis/html/html-attribute-security-pass.js +14 -9
  12. package/dist/analysis/html/html-attribute-security-pass.js.map +1 -1
  13. package/dist/analysis/html/html-extractor.d.ts.map +1 -1
  14. package/dist/analysis/html/html-extractor.js +16 -11
  15. package/dist/analysis/html/html-extractor.js.map +1 -1
  16. package/dist/analysis/passes/insecure-cookie-pass.d.ts +53 -0
  17. package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -0
  18. package/dist/analysis/passes/insecure-cookie-pass.js +109 -0
  19. package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -0
  20. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  21. package/dist/analysis/passes/interprocedural-pass.js +7 -0
  22. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  23. package/dist/analysis/passes/language-sources-pass.d.ts +14 -0
  24. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.js +50 -0
  26. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  27. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/sink-filter-pass.js +21 -2
  29. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  30. package/dist/analysis/passes/taint-propagation-pass.js +94 -3
  31. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  32. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  33. package/dist/analysis/taint-matcher.js +117 -20
  34. package/dist/analysis/taint-matcher.js.map +1 -1
  35. package/dist/analyzer.d.ts.map +1 -1
  36. package/dist/analyzer.js +3 -0
  37. package/dist/analyzer.js.map +1 -1
  38. package/dist/browser/circle-ir.js +453 -99
  39. package/dist/core/circle-ir-core.cjs +251 -64
  40. package/dist/core/circle-ir-core.js +251 -64
  41. package/dist/core/extractors/types.js +85 -2
  42. package/dist/core/extractors/types.js.map +1 -1
  43. package/dist/core/parser.d.ts +10 -0
  44. package/dist/core/parser.d.ts.map +1 -1
  45. package/dist/core/parser.js +20 -5
  46. package/dist/core/parser.js.map +1 -1
  47. package/dist/languages/plugins/base.d.ts.map +1 -1
  48. package/dist/languages/plugins/base.js +15 -11
  49. package/dist/languages/plugins/base.js.map +1 -1
  50. package/dist/languages/plugins/java.d.ts.map +1 -1
  51. package/dist/languages/plugins/java.js +8 -4
  52. package/dist/languages/plugins/java.js.map +1 -1
  53. package/package.json +1 -1
@@ -4127,11 +4127,13 @@ function disposeTree(tree) {
4127
4127
  }
4128
4128
  }
4129
4129
  function walkTree(node, visitor) {
4130
- visitor(node);
4131
- for (let i2 = 0; i2 < node.childCount; i2++) {
4132
- const child = node.child(i2);
4133
- if (child) {
4134
- walkTree(child, visitor);
4130
+ const stack = [node];
4131
+ while (stack.length > 0) {
4132
+ const current = stack.pop();
4133
+ visitor(current);
4134
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
4135
+ const child = current.child(i2);
4136
+ if (child) stack.push(child);
4135
4137
  }
4136
4138
  }
4137
4139
  }
@@ -4750,14 +4752,46 @@ function extractJSClassInfo(node) {
4750
4752
  end_line: node.endPosition.row + 1
4751
4753
  };
4752
4754
  }
4755
+ function extractDecoratorName(node) {
4756
+ const child = node.namedChildCount > 0 ? node.namedChild(0) : null;
4757
+ if (!child) return null;
4758
+ if (child.type === "identifier") return getNodeText(child);
4759
+ if (child.type === "call_expression") {
4760
+ const fn = child.childForFieldName("function");
4761
+ if (fn) {
4762
+ if (fn.type === "identifier") return getNodeText(fn);
4763
+ if (fn.type === "member_expression") {
4764
+ const propNode = fn.childForFieldName("property");
4765
+ if (propNode) return getNodeText(propNode);
4766
+ }
4767
+ }
4768
+ }
4769
+ if (child.type === "member_expression") {
4770
+ const propNode = child.childForFieldName("property");
4771
+ if (propNode) return getNodeText(propNode);
4772
+ }
4773
+ return null;
4774
+ }
4753
4775
  function extractJSMethods(body2) {
4754
4776
  const methods = [];
4777
+ let pendingDecorators = [];
4755
4778
  for (let i2 = 0; i2 < body2.childCount; i2++) {
4756
4779
  const child = body2.child(i2);
4757
4780
  if (!child) continue;
4781
+ if (child.type === "decorator") {
4782
+ const name2 = extractDecoratorName(child);
4783
+ if (name2) pendingDecorators.push(name2);
4784
+ continue;
4785
+ }
4786
+ if (child.type === "comment") continue;
4758
4787
  if (child.type === "method_definition") {
4759
- methods.push(extractJSMethodInfo(child));
4788
+ const m = extractJSMethodInfo(child);
4789
+ if (pendingDecorators.length > 0) {
4790
+ m.annotations = pendingDecorators;
4791
+ }
4792
+ methods.push(m);
4760
4793
  }
4794
+ pendingDecorators = [];
4761
4795
  }
4762
4796
  return methods;
4763
4797
  }
@@ -4919,10 +4953,18 @@ function extractJSParameters(params) {
4919
4953
  if (typeNode) {
4920
4954
  paramType = getNodeText(typeNode).replace(/^:\s*/, "");
4921
4955
  }
4956
+ const decorators = [];
4957
+ for (let j = 0; j < child.childCount; j++) {
4958
+ const c = child.child(j);
4959
+ if (c && c.type === "decorator") {
4960
+ const name2 = extractDecoratorName(c);
4961
+ if (name2) decorators.push(name2);
4962
+ }
4963
+ }
4922
4964
  parameters.push({
4923
4965
  name: paramName,
4924
4966
  type: paramType,
4925
- annotations: [],
4967
+ annotations: decorators,
4926
4968
  line: child.startPosition.row + 1
4927
4969
  });
4928
4970
  }
@@ -11633,6 +11675,26 @@ var DEFAULT_SINKS = [
11633
11675
  { method: "setQuotedArgumentsEnabled", class: "Shell", type: "command_injection", cwe: "CWE-78", severity: "high", arg_positions: [0] },
11634
11676
  // Sandbox/script security
11635
11677
  { method: "onNewInstance", class: "SandboxInterceptor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
11678
+ // Java Log Injection (slf4j / logback / java.util.logging) — CWE-117
11679
+ // Issue #44: log.info/warn/error/debug emit the message argument and any
11680
+ // {} format arguments to the log stream. Untrusted input forwarded into
11681
+ // these calls allows log forging (newline injection) and downstream log
11682
+ // analyzer pollution. Scoped to `java` so the generic method names don't
11683
+ // collide with JS console / Python logger entries below.
11684
+ { method: "info", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11685
+ { method: "warn", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11686
+ { method: "error", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11687
+ { method: "debug", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11688
+ { method: "trace", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["java"] },
11689
+ // java.util.logging.Logger uses the same class name `Logger` — same entries above cover it.
11690
+ // Severity-tagged levels: SEVERE/WARNING/INFO/CONFIG/FINE/FINER/FINEST
11691
+ { method: "severe", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11692
+ { method: "warning", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11693
+ { method: "config", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11694
+ { method: "fine", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11695
+ { method: "finer", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11696
+ { method: "finest", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0], languages: ["java"] },
11697
+ { method: "log", class: "Logger", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [1, 2, 3], languages: ["java"] },
11636
11698
  // =========================================================================
11637
11699
  // Node.js/Express Sinks
11638
11700
  // =========================================================================
@@ -11685,13 +11747,47 @@ var DEFAULT_SINKS = [
11685
11747
  { method: "runInContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11686
11748
  { method: "runInNewContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11687
11749
  { method: "runInThisContext", class: "vm", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
11688
- // Node.js NoSQL Injection (MongoDB)
11750
+ // Node.js NoSQL Injection (MongoDB native driver + mongoose) — CWE-943
11751
+ // Issue #45: the bare `class: 'Collection'` constraint missed mongoose's
11752
+ // fluent chains (mongoose.connection.db.collection('x').find({...})) and
11753
+ // Model.find calls because the call-site receiver type does not resolve
11754
+ // to `Collection`. Add classless+language-scoped entries for the
11755
+ // MongoDB-specific method names (findOne/aggregate/updateOne/etc.) and
11756
+ // mongoose `Model`/`Query` class entries. Bare `find` stays class-scoped
11757
+ // to avoid colliding with Array.prototype.find.
11689
11758
  { method: "find", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11690
11759
  { method: "findOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11691
11760
  { method: "updateOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11692
11761
  { method: "updateMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11693
11762
  { method: "deleteOne", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11694
11763
  { method: "deleteMany", class: "Collection", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0] },
11764
+ // Mongoose Model/Query class entries — Model.find/findOne/etc.
11765
+ { method: "find", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11766
+ { method: "findOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11767
+ { method: "findById", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11768
+ { method: "findOneAndUpdate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11769
+ { method: "findOneAndDelete", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11770
+ { method: "findOneAndReplace", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11771
+ { method: "updateOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11772
+ { method: "updateMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11773
+ { method: "deleteOne", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11774
+ { method: "deleteMany", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11775
+ { method: "countDocuments", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11776
+ { method: "aggregate", class: "Model", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11777
+ // Mongoose Query class entries — chain methods returning Query
11778
+ { method: "where", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11779
+ { method: "equals", class: "Query", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11780
+ // Classless MongoDB-specific method names (rare outside MongoDB APIs) —
11781
+ // language-scoped to JS/TS. Excludes plain `find` (Array.prototype.find FP).
11782
+ { method: "findOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11783
+ { method: "findOneAndUpdate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11784
+ { method: "findOneAndDelete", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11785
+ { method: "findOneAndReplace", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11786
+ { method: "updateOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11787
+ { method: "updateMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0, 1], languages: ["javascript", "typescript"] },
11788
+ { method: "deleteOne", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11789
+ { method: "deleteMany", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11790
+ { method: "aggregate", type: "nosql_injection", cwe: "CWE-943", severity: "high", arg_positions: [0], languages: ["javascript", "typescript"] },
11695
11791
  // Node.js SSRF (HTTP clients)
11696
11792
  { method: "get", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11697
11793
  { method: "post", class: "axios", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
@@ -11713,6 +11809,24 @@ var DEFAULT_SINKS = [
11713
11809
  { method: "post", class: "superagent", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11714
11810
  // node-fetch
11715
11811
  { method: "default", class: "node-fetch", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0] },
11812
+ // Node.js / JavaScript Log Injection (console.*) — CWE-117
11813
+ // Issue #44: console.log/warn/error/info with tainted template literals
11814
+ // allow log forging (newline-injection) and downstream log analyzer
11815
+ // pollution. Scoped to JS/TS so the bare class `console` doesn't collide
11816
+ // with Python `console` module or Java identifiers.
11817
+ { method: "log", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11818
+ { method: "warn", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11819
+ { method: "error", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11820
+ { method: "info", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11821
+ { method: "debug", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11822
+ { method: "trace", class: "console", type: "log_injection", cwe: "CWE-117", severity: "low", arg_positions: [0, 1, 2, 3], languages: ["javascript", "typescript"] },
11823
+ // Node.js / Express Open Redirect — CWE-601
11824
+ // Issue #46: `res.redirect(req.query.next)` did not fire because the
11825
+ // legacy `class: 'Response'` constraint depended on receiver type
11826
+ // resolution of the Express `res` parameter. Mirror Python's classless
11827
+ // pattern with a language-scoped classless entry. The method name
11828
+ // `redirect` is rare outside HTTP frameworks so the FP risk is low.
11829
+ { method: "redirect", type: "open_redirect", cwe: "CWE-601", severity: "medium", arg_positions: [0], languages: ["javascript", "typescript"] },
11716
11830
  // =========================================================================
11717
11831
  // Python Sinks
11718
11832
  // =========================================================================
@@ -11754,7 +11868,12 @@ var DEFAULT_SINKS = [
11754
11868
  { method: "rmtree", class: "shutil", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0] },
11755
11869
  { method: "send_file", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
11756
11870
  // Python XSS / SSTI
11757
- { method: "render_template_string", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11871
+ // Issue #54: Flask's `render_template_string(template_str)` with an
11872
+ // attacker-controlled template string is Server-Side Template Injection
11873
+ // (Jinja2 SSTI → RCE), not reflected XSS. Classify as code_injection
11874
+ // (CWE-94) with critical severity to match `jinja2.Template().render()`
11875
+ // and `Template.from_string()` entries above.
11876
+ { method: "render_template_string", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["python"] },
11758
11877
  { method: "Markup", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11759
11878
  { method: "mark_safe", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["python"] },
11760
11879
  // Python SSRF
@@ -12117,6 +12236,13 @@ var DEFAULT_SANITIZERS = [
12117
12236
  { method: "secure_filename", class: "werkzeug.utils", removes: ["path_traversal"] },
12118
12237
  { method: "basename", class: "os.path", removes: ["path_traversal"] },
12119
12238
  { method: "normpath", class: "os.path", removes: ["path_traversal"] },
12239
+ // Issue #48 part 2: realpath/abspath are canonical Python path-canonicalization
12240
+ // functions (analogous to Java File.getCanonicalPath). Register on both
12241
+ // `os.path` and the bare `path` receiver to cover `import os.path as path`.
12242
+ { method: "realpath", class: "os.path", removes: ["path_traversal"] },
12243
+ { method: "abspath", class: "os.path", removes: ["path_traversal"] },
12244
+ { method: "realpath", class: "path", removes: ["path_traversal"] },
12245
+ { method: "abspath", class: "path", removes: ["path_traversal"] },
12120
12246
  // Python Type coercion
12121
12247
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
12122
12248
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -12265,7 +12391,7 @@ var PYTHON_TAINTED_PATTERNS = [
12265
12391
  ];
12266
12392
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
12267
12393
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
12268
- const sources = findSources(calls, types, config.sources, sourceLines);
12394
+ const sources = findSources(calls, types, config.sources, sourceLines, language);
12269
12395
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
12270
12396
  const sanitizers = findSanitizers(calls, types, config.sanitizers);
12271
12397
  return { sources, sinks, sanitizers };
@@ -12283,7 +12409,7 @@ function attachSourceLineCode(sources, sinks, code) {
12283
12409
  }
12284
12410
  }
12285
12411
  }
12286
- function findSources(calls, types, patterns, sourceLines) {
12412
+ function findSources(calls, types, patterns, sourceLines, language) {
12287
12413
  const sources = [];
12288
12414
  for (const call of calls) {
12289
12415
  for (const pattern of patterns) {
@@ -12336,23 +12462,29 @@ function findSources(calls, types, patterns, sourceLines) {
12336
12462
  }
12337
12463
  }
12338
12464
  }
12339
- const RUST_EXTRACTOR_TYPES = /^(?:Json|Form|Query|Path|Extension|Multipart)(?:<|$)|^(?:Body|Bytes)$/;
12465
+ const RUST_EXTRACTOR_KIND = /(?:^|::)(Json|Form|Query|Path|Extension|Multipart|Body|Bytes)(?:<|$)/;
12340
12466
  for (const type of types) {
12341
12467
  for (const method of type.methods) {
12342
12468
  for (const param of method.parameters) {
12343
- if (param.type && RUST_EXTRACTOR_TYPES.test(param.type)) {
12344
- const paramLine = param.line ?? method.start_line;
12345
- const alreadyExists = sources.some((s) => s.line === paramLine && s.type === "http_body");
12346
- if (!alreadyExists) {
12347
- sources.push({
12348
- type: "http_body",
12349
- location: `${param.type} ${param.name} in ${method.name}`,
12350
- severity: "high",
12351
- line: paramLine,
12352
- confidence: 1
12353
- });
12354
- }
12355
- }
12469
+ if (!param.type) continue;
12470
+ const kindMatch = RUST_EXTRACTOR_KIND.exec(param.type);
12471
+ if (!kindMatch) continue;
12472
+ const kind = kindMatch[1];
12473
+ if (kind === "Extension") continue;
12474
+ const sourceType = kind === "Form" || kind === "Query" || kind === "Path" ? "http_param" : "http_body";
12475
+ const paramLine = param.line ?? method.start_line;
12476
+ const alreadyExists = sources.some(
12477
+ (s) => s.line === paramLine && s.variable === param.name
12478
+ );
12479
+ if (alreadyExists) continue;
12480
+ sources.push({
12481
+ type: sourceType,
12482
+ location: `${param.type} ${param.name} in ${method.name}`,
12483
+ severity: "high",
12484
+ line: paramLine,
12485
+ confidence: 1,
12486
+ variable: param.name
12487
+ });
12356
12488
  }
12357
12489
  }
12358
12490
  }
@@ -12434,6 +12566,15 @@ function findSources(calls, types, patterns, sourceLines) {
12434
12566
  s.code = sourceLines[s.line - 1]?.trim();
12435
12567
  }
12436
12568
  }
12569
+ if (language === "rust" && sourceLines) {
12570
+ const LET_BINDING = /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=/;
12571
+ for (const s of result) {
12572
+ if (s.variable && s.variable.length > 0) continue;
12573
+ const lineText = sourceLines[s.line - 1] ?? "";
12574
+ const m = LET_BINDING.exec(lineText);
12575
+ if (m) s.variable = m[1];
12576
+ }
12577
+ }
12437
12578
  return result;
12438
12579
  }
12439
12580
  function isInterproceduralTaintableType(typeName) {
@@ -12539,6 +12680,20 @@ function isParameterizedQueryCall(call, pattern) {
12539
12680
  }
12540
12681
  return false;
12541
12682
  }
12683
+ function isSafePythonSubprocessCall(call, pattern, language) {
12684
+ if (language !== "python") return false;
12685
+ if (pattern.type !== "command_injection") return false;
12686
+ if (pattern.class !== "subprocess") return false;
12687
+ const arg0 = call.arguments.find((a) => a.position === 0);
12688
+ if (!arg0) return false;
12689
+ const expr0 = (arg0.literal ?? arg0.expression ?? "").trim();
12690
+ if (!expr0.startsWith("[")) return false;
12691
+ for (const a of call.arguments) {
12692
+ const e = (a.expression ?? "").trim();
12693
+ if (/^shell\s*=\s*True\b/.test(e)) return false;
12694
+ }
12695
+ return true;
12696
+ }
12542
12697
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12543
12698
  function argIsClassLiteral(call, position) {
12544
12699
  const arg = call.arguments.find((a) => a.position === position);
@@ -12555,6 +12710,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12555
12710
  if (isParameterizedQueryCall(call, pattern)) {
12556
12711
  continue;
12557
12712
  }
12713
+ if (isSafePythonSubprocessCall(call, pattern, language)) {
12714
+ continue;
12715
+ }
12558
12716
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12559
12717
  continue;
12560
12718
  }
@@ -12957,7 +13115,12 @@ function receiverMightBeClass(receiver, className) {
12957
13115
  "controller",
12958
13116
  "task",
12959
13117
  "thread",
12960
- "job"
13118
+ "job",
13119
+ // Short Python DB abbreviation; would otherwise prefix-match obscure XSS
13120
+ // sink classes like XWiki's `CurrentTimePlugin` ('current'.startsWith('cur'))
13121
+ // via the CamelCase word prefix heuristic and produce an xss FP on every
13122
+ // `cur.execute(...)`. Resolved via commonMappings → ['Cursor']. See #65 / #48 pt3.
13123
+ "cur"
12961
13124
  ]);
12962
13125
  const isAmbiguous = ambiguousIdentifiers.has(lowerReceiver);
12963
13126
  if (!isAmbiguous && lowerReceiver.length >= 3 && lowerClass.includes(lowerReceiver)) {
@@ -12967,7 +13130,9 @@ function receiverMightBeClass(receiver, className) {
12967
13130
  }
12968
13131
  if (!isAmbiguous && lowerReceiver.length >= 2) {
12969
13132
  if (lowerClass.startsWith(lowerReceiver) || lowerClass.endsWith(lowerReceiver)) {
12970
- return true;
13133
+ if (lowerReceiver.length / lowerClass.length >= 0.4) {
13134
+ return true;
13135
+ }
12971
13136
  }
12972
13137
  }
12973
13138
  if (!isAmbiguous && lowerReceiver.length >= 3) {
@@ -12990,6 +13155,9 @@ function receiverMightBeClass(receiver, className) {
12990
13155
  ps: ["PreparedStatement"],
12991
13156
  rs: ["ResultSet"],
12992
13157
  template: ["JdbcTemplate"],
13158
+ cur: ["Cursor"],
13159
+ // Python DB-API cursor — see ambiguousIdentifiers note
13160
+ cursor: ["Cursor"],
12993
13161
  // I/O
12994
13162
  writer: ["PrintWriter"],
12995
13163
  out: ["PrintWriter", "OutputStream"],
@@ -16032,8 +16200,10 @@ var ConstantPropagator = class _ConstantPropagator {
16032
16200
  * These are variables declared directly in the class body, not inside methods.
16033
16201
  */
16034
16202
  collectClassFields(root) {
16035
- const traverse = (n, inClass, inMethod) => {
16036
- if (!n) return;
16203
+ const stack = [root];
16204
+ while (stack.length > 0) {
16205
+ const n = stack.pop();
16206
+ if (!n) continue;
16037
16207
  if (n.type === "class_body") {
16038
16208
  for (const child of n.children) {
16039
16209
  if (child.type === "field_declaration") {
@@ -16047,32 +16217,28 @@ var ConstantPropagator = class _ConstantPropagator {
16047
16217
  }
16048
16218
  }
16049
16219
  }
16050
- if (child.type === "method_declaration" || child.type === "constructor_declaration") {
16051
- traverse(child, true, true);
16052
- } else {
16053
- traverse(child, true, false);
16054
- }
16220
+ stack.push(child);
16055
16221
  }
16056
- return;
16222
+ continue;
16057
16223
  }
16058
16224
  for (const child of n.children) {
16059
- traverse(child, inClass, inMethod);
16225
+ stack.push(child);
16060
16226
  }
16061
- };
16062
- traverse(root, false, false);
16227
+ }
16063
16228
  }
16064
16229
  findAllMethods(node) {
16065
16230
  const methods = [];
16066
- const traverse = (n) => {
16067
- if (!n) return;
16231
+ const stack = [node];
16232
+ while (stack.length > 0) {
16233
+ const n = stack.pop();
16234
+ if (!n) continue;
16068
16235
  if (n.type === "method_declaration" || n.type === "function_declaration") {
16069
16236
  methods.push(n);
16070
16237
  }
16071
16238
  for (const child of n.children) {
16072
- if (child) traverse(child);
16239
+ if (child) stack.push(child);
16073
16240
  }
16074
- };
16075
- traverse(node);
16241
+ }
16076
16242
  return methods;
16077
16243
  }
16078
16244
  getMethodName(method) {
@@ -16123,9 +16289,24 @@ var ConstantPropagator = class _ConstantPropagator {
16123
16289
  // AST Visitor
16124
16290
  // ===========================================================================
16125
16291
  visit(node) {
16292
+ const stack = [node];
16293
+ while (stack.length > 0) {
16294
+ const current = stack.pop();
16295
+ if (this.visitOne(current)) continue;
16296
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
16297
+ stack.push(current.children[i2]);
16298
+ }
16299
+ }
16300
+ }
16301
+ /**
16302
+ * Visit a single node. Returns true if the handler already descended into
16303
+ * children (and the caller should NOT push them), false to fall through to
16304
+ * the default pre-order descent.
16305
+ */
16306
+ visitOne(node) {
16126
16307
  const line = getNodeLine(node);
16127
16308
  if (this.unreachableLines.has(line)) {
16128
- return;
16309
+ return true;
16129
16310
  }
16130
16311
  if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
16131
16312
  this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
@@ -16134,43 +16315,41 @@ var ConstantPropagator = class _ConstantPropagator {
16134
16315
  case "method_declaration":
16135
16316
  case "constructor_declaration":
16136
16317
  this.handleMethodDeclaration(node);
16137
- return;
16318
+ return true;
16138
16319
  // Don't visit children directly, handleMethodDeclaration does it
16139
16320
  case "local_variable_declaration":
16140
16321
  this.handleVariableDeclaration(node);
16141
- break;
16322
+ return false;
16142
16323
  case "assignment_expression":
16143
16324
  this.handleAssignment(node);
16144
- break;
16325
+ return false;
16145
16326
  case "update_expression":
16146
16327
  this.handleUpdateExpression(node);
16147
- break;
16328
+ return false;
16148
16329
  case "if_statement":
16149
16330
  this.handleIfStatement(node);
16150
- return;
16331
+ return true;
16151
16332
  case "switch_expression":
16152
16333
  case "switch_statement":
16153
16334
  this.handleSwitch(node);
16154
- return;
16335
+ return true;
16155
16336
  case "ternary_expression":
16156
16337
  this.handleTernary(node);
16157
- break;
16338
+ return false;
16158
16339
  case "expression_statement":
16159
16340
  this.handleExpressionStatement(node);
16160
- break;
16341
+ return false;
16161
16342
  case "for_statement":
16162
16343
  case "enhanced_for_statement":
16163
16344
  case "while_statement":
16164
16345
  case "do_statement":
16165
16346
  this.handleLoopStatement(node);
16166
- return;
16347
+ return true;
16167
16348
  case "synchronized_statement":
16168
16349
  this.handleSynchronizedStatement(node);
16169
- return;
16350
+ return true;
16170
16351
  default:
16171
- for (const child of node.children) {
16172
- this.visit(child);
16173
- }
16352
+ return false;
16174
16353
  }
16175
16354
  }
16176
16355
  /**
@@ -16938,6 +17117,19 @@ var ConstantPropagator = class _ConstantPropagator {
16938
17117
  return null;
16939
17118
  }
16940
17119
  isTaintedExpression(node) {
17120
+ const stack = [node];
17121
+ while (stack.length > 0) {
17122
+ const current = stack.pop();
17123
+ const result = this.isTaintedExpressionStep(current);
17124
+ if (result === true) return true;
17125
+ if (result === false) continue;
17126
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
17127
+ stack.push(current.children[i2]);
17128
+ }
17129
+ }
17130
+ return false;
17131
+ }
17132
+ isTaintedExpressionStep(node) {
16941
17133
  const text = getNodeText2(node, this.source);
16942
17134
  if (node.type === "method_invocation") {
16943
17135
  const nameNode = node.childForFieldName("name");
@@ -17197,12 +17389,7 @@ var ConstantPropagator = class _ConstantPropagator {
17197
17389
  }
17198
17390
  return isTainted;
17199
17391
  }
17200
- for (const child of node.children) {
17201
- if (this.isTaintedExpression(child)) {
17202
- return true;
17203
- }
17204
- }
17205
- return false;
17392
+ return void 0;
17206
17393
  }
17207
17394
  checkCollectionTaint(node) {
17208
17395
  const objectNode = node.childForFieldName("object");
@@ -17373,7 +17560,7 @@ function isFalsePositive(result, sinkLine, taintedVar) {
17373
17560
  if (varValue && varValue.type !== "unknown" && !result.tainted.has(taintedVar)) {
17374
17561
  return { isFalsePositive: true, reason: `variable_is_constant: ${varValue.value}` };
17375
17562
  }
17376
- if (result.symbols.size > 0 && !result.tainted.has(taintedVar)) {
17563
+ if (result.symbols.has(taintedVar) && !result.tainted.has(taintedVar)) {
17377
17564
  return { isFalsePositive: true, reason: "variable_not_tainted" };
17378
17565
  }
17379
17566
  return { isFalsePositive: false, reason: null };
@@ -17526,19 +17713,17 @@ var BaseLanguagePlugin = class {
17526
17713
  */
17527
17714
  findNodes(root, type) {
17528
17715
  const nodes = [];
17529
- const cursor = root.walk();
17530
- const visit = () => {
17531
- if (cursor.nodeType === type) {
17532
- nodes.push(cursor.currentNode);
17716
+ const stack = [root];
17717
+ while (stack.length > 0) {
17718
+ const node = stack.pop();
17719
+ if (node.type === type) {
17720
+ nodes.push(node);
17533
17721
  }
17534
- if (cursor.gotoFirstChild()) {
17535
- do {
17536
- visit();
17537
- } while (cursor.gotoNextSibling());
17538
- cursor.gotoParent();
17722
+ for (let i2 = node.childCount - 1; i2 >= 0; i2--) {
17723
+ const child = node.child(i2);
17724
+ if (child) stack.push(child);
17539
17725
  }
17540
- };
17541
- visit();
17726
+ }
17542
17727
  return nodes;
17543
17728
  }
17544
17729
  /**
@@ -17894,16 +18079,17 @@ var JavaPlugin = class extends BaseLanguagePlugin {
17894
18079
  }
17895
18080
  }
17896
18081
  };
17897
- const walk = (node) => {
18082
+ const stack = [tree.rootNode];
18083
+ while (stack.length > 0) {
18084
+ const node = stack.pop();
17898
18085
  if (node.type === "field_declaration" || node.type === "local_variable_declaration") {
17899
18086
  collectDecl(node);
17900
18087
  }
17901
18088
  for (let i2 = 0; i2 < node.childCount; i2++) {
17902
18089
  const child = node.child(i2);
17903
- if (child) walk(child);
18090
+ if (child) stack.push(child);
17904
18091
  }
17905
- };
17906
- walk(tree.rootNode);
18092
+ }
17907
18093
  this._typeMapCache.set(tree, map);
17908
18094
  return map;
17909
18095
  }
@@ -20700,16 +20886,18 @@ function extractHtmlContent(rootNode) {
20700
20886
  return { scriptBlocks, eventHandlers };
20701
20887
  }
20702
20888
  function walkNode(node, scriptBlocks, eventHandlers) {
20703
- if (node.type === "script_element") {
20704
- extractScriptBlock(node, scriptBlocks);
20705
- }
20706
- if (node.type === "element" || node.type === "self_closing_tag") {
20707
- extractEventHandlers(node, eventHandlers);
20708
- }
20709
- for (let i2 = 0; i2 < node.childCount; i2++) {
20710
- const child = node.child(i2);
20711
- if (child) {
20712
- walkNode(child, scriptBlocks, eventHandlers);
20889
+ const stack = [node];
20890
+ while (stack.length > 0) {
20891
+ const current = stack.pop();
20892
+ if (current.type === "script_element") {
20893
+ extractScriptBlock(current, scriptBlocks);
20894
+ }
20895
+ if (current.type === "element" || current.type === "self_closing_tag") {
20896
+ extractEventHandlers(current, eventHandlers);
20897
+ }
20898
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
20899
+ const child = current.child(i2);
20900
+ if (child) stack.push(child);
20713
20901
  }
20714
20902
  }
20715
20903
  }
@@ -20806,13 +20994,15 @@ function runHtmlAttributeSecurityChecks(rootNode, filePath) {
20806
20994
  return findings;
20807
20995
  }
20808
20996
  function walkForSecurityChecks(node, filePath, findings) {
20809
- if (node.type === "element" || node.type === "self_closing_tag" || node.type === "script_element" || node.type === "style_element") {
20810
- checkElement(node, filePath, findings);
20811
- }
20812
- for (let i2 = 0; i2 < node.childCount; i2++) {
20813
- const child = node.child(i2);
20814
- if (child) {
20815
- walkForSecurityChecks(child, filePath, findings);
20997
+ const stack = [node];
20998
+ while (stack.length > 0) {
20999
+ const current = stack.pop();
21000
+ if (current.type === "element" || current.type === "self_closing_tag" || current.type === "script_element" || current.type === "style_element") {
21001
+ checkElement(current, filePath, findings);
21002
+ }
21003
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
21004
+ const child = current.child(i2);
21005
+ if (child) stack.push(child);
20816
21006
  }
20817
21007
  }
20818
21008
  }
@@ -21688,6 +21878,37 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
21688
21878
  }
21689
21879
  return tainted;
21690
21880
  }
21881
+ function buildRustTaintedVars(sourceCode, seedVars) {
21882
+ const derived = /* @__PURE__ */ new Map();
21883
+ const knownTainted = new Set(seedVars);
21884
+ const lines = sourceCode.split("\n");
21885
+ let changed = true;
21886
+ while (changed) {
21887
+ changed = false;
21888
+ for (let i2 = 0; i2 < lines.length; i2++) {
21889
+ const line = lines[i2];
21890
+ const trimmed = line.trimStart();
21891
+ if (trimmed.startsWith("//")) continue;
21892
+ const letMatch = line.match(
21893
+ /^\s*let\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::\s*[^=]+)?=\s*(.+?)(?:;|$)/
21894
+ );
21895
+ const assignMatch = !letMatch ? line.match(/^\s*([A-Za-z_]\w*)\s*=\s*(.+?)(?:;|$)/) : null;
21896
+ const m = letMatch ?? assignMatch;
21897
+ if (!m) continue;
21898
+ const lhs = m[1];
21899
+ const rhs = m[2];
21900
+ if (lhs === "if" || lhs === "while" || lhs === "for" || lhs === "match" || lhs === "return") continue;
21901
+ if (knownTainted.has(lhs)) continue;
21902
+ const ref = [...knownTainted].some((v) => new RegExp(`\\b${v}\\b`).test(rhs));
21903
+ if (ref) {
21904
+ derived.set(lhs, i2 + 1);
21905
+ knownTainted.add(lhs);
21906
+ changed = true;
21907
+ }
21908
+ }
21909
+ }
21910
+ return derived;
21911
+ }
21691
21912
  var BASH_UNTRUSTED_ENV_PATTERNS = [
21692
21913
  /^USER_INPUT$/i,
21693
21914
  /^QUERY_STRING$/i,
@@ -22087,7 +22308,20 @@ function evaluateSimpleExpression(expr, symbols) {
22087
22308
  }
22088
22309
  function isStringLiteralExpression(expr) {
22089
22310
  const trimmed = expr.trim();
22090
- return trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'");
22311
+ if (trimmed.length < 2) return false;
22312
+ const quote = trimmed[0];
22313
+ if (quote !== '"' && quote !== "'") return false;
22314
+ let i2 = 1;
22315
+ while (i2 < trimmed.length) {
22316
+ const c = trimmed[i2];
22317
+ if (c === "\\") {
22318
+ i2 += 2;
22319
+ continue;
22320
+ }
22321
+ if (c === quote) return i2 === trimmed.length - 1;
22322
+ i2++;
22323
+ }
22324
+ return false;
22091
22325
  }
22092
22326
  function filterCleanArraySinks(sinks, calls, taintedArrayElements, symbols) {
22093
22327
  const callsByLine = /* @__PURE__ */ new Map();
@@ -22272,7 +22506,7 @@ var TaintPropagationPass = class {
22272
22506
  flows.push(f);
22273
22507
  }
22274
22508
  }
22275
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
22509
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
22276
22510
  for (const f of exprScanFlows) {
22277
22511
  if (flows.some(
22278
22512
  (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
@@ -22484,14 +22718,63 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22484
22718
  void types;
22485
22719
  return flows;
22486
22720
  }
22487
- function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code, language) {
22721
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
22488
22722
  const flows = [];
22489
22723
  const sourcesWithVar = sources.filter(
22490
22724
  (s) => typeof s.variable === "string" && s.variable.length > 0
22491
22725
  );
22492
22726
  if (sourcesWithVar.length === 0) return flows;
22727
+ const aliasSanitizedFor = /* @__PURE__ */ new Map();
22493
22728
  if (language === "python" && typeof code === "string") {
22494
22729
  const derived = buildPythonTaintedVars(code);
22730
+ if (derived.size > 0) {
22731
+ let anchor = sourcesWithVar[0];
22732
+ for (const s of sourcesWithVar) {
22733
+ if (s.line < anchor.line) anchor = s;
22734
+ }
22735
+ const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
22736
+ for (const [varName] of derived) {
22737
+ if (!varName || existingVars.has(varName)) continue;
22738
+ sourcesWithVar.push({
22739
+ ...anchor,
22740
+ variable: varName
22741
+ });
22742
+ existingVars.add(varName);
22743
+ }
22744
+ if (sanitizers && sanitizers.length > 0) {
22745
+ const sanitizersByLine = /* @__PURE__ */ new Map();
22746
+ for (const s of sanitizers) {
22747
+ const arr = sanitizersByLine.get(s.line) ?? [];
22748
+ arr.push(s);
22749
+ sanitizersByLine.set(s.line, arr);
22750
+ }
22751
+ const codeLines = code.split("\n");
22752
+ for (const [varName, originLine] of derived) {
22753
+ const lineSans = sanitizersByLine.get(originLine);
22754
+ if (!lineSans || lineSans.length === 0) continue;
22755
+ const lineText = codeLines[originLine - 1] ?? "";
22756
+ const rhsMatch = lineText.match(/^\s*\w+\s*=\s*(.+)$/);
22757
+ if (!rhsMatch) continue;
22758
+ const rhs = rhsMatch[1];
22759
+ for (const san of lineSans) {
22760
+ const sanMatch = san.method.match(/^(?:(\w+)\.)?(\w+)\(\)$/);
22761
+ if (!sanMatch) continue;
22762
+ const sanName = sanMatch[1] ? `${sanMatch[1]}.${sanMatch[2]}` : sanMatch[2];
22763
+ if (!rhs.includes(`${sanName}(`)) continue;
22764
+ let set = aliasSanitizedFor.get(varName);
22765
+ if (!set) {
22766
+ set = /* @__PURE__ */ new Set();
22767
+ aliasSanitizedFor.set(varName, set);
22768
+ }
22769
+ for (const t of san.sanitizes) set.add(t);
22770
+ }
22771
+ }
22772
+ }
22773
+ }
22774
+ }
22775
+ if (language === "rust" && typeof code === "string") {
22776
+ const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
22777
+ const derived = buildRustTaintedVars(code, seedVars);
22495
22778
  if (derived.size > 0) {
22496
22779
  let anchor = sourcesWithVar[0];
22497
22780
  for (const s of sourcesWithVar) {
@@ -22537,6 +22820,9 @@ function detectExpressionScanFlows(calls, sources, sinks, unreachableLines, code
22537
22820
  if (flows.some(
22538
22821
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
22539
22822
  )) continue;
22823
+ if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
22824
+ break;
22825
+ }
22540
22826
  flows.push({
22541
22827
  source_line: source.line,
22542
22828
  sink_line: sink.line,
@@ -22681,6 +22967,9 @@ var InterproceduralPass = class {
22681
22967
  }
22682
22968
  }
22683
22969
  }
22970
+ if (additionalSinks.length > 0) {
22971
+ attachSourceLineCode([], additionalSinks, ctx.code);
22972
+ }
22684
22973
  return { additionalSinks, additionalFlows, interprocedural };
22685
22974
  }
22686
22975
  };
@@ -26679,6 +26968,70 @@ function isPotentialPojo(type) {
26679
26968
  return first >= 65 && first <= 90;
26680
26969
  }
26681
26970
 
26971
+ // src/analysis/passes/insecure-cookie-pass.ts
26972
+ var COOKIE_RESPONSE_RECEIVERS = /* @__PURE__ */ new Set([
26973
+ "res",
26974
+ "response",
26975
+ "reply"
26976
+ ]);
26977
+ var SECURE_TRUE_RE = /\bsecure\s*:\s*true\b/;
26978
+ var HTTPONLY_TRUE_RE = /\bhttpOnly\s*:\s*true\b/i;
26979
+ var InsecureCookiePass = class {
26980
+ name = "insecure-cookie";
26981
+ category = "security";
26982
+ run(ctx) {
26983
+ const { graph, language } = ctx;
26984
+ if (language !== "javascript" && language !== "typescript") {
26985
+ return { insecureCookies: [] };
26986
+ }
26987
+ const file = graph.ir.meta.file;
26988
+ const insecureCookies = [];
26989
+ for (const call of graph.ir.calls) {
26990
+ if (call.method_name !== "cookie") continue;
26991
+ const receiver = call.receiver ?? "";
26992
+ if (!COOKIE_RESPONSE_RECEIVERS.has(receiver)) continue;
26993
+ if (call.arguments.length < 2) continue;
26994
+ const opts = call.arguments.find((a) => a.position === 2);
26995
+ const optsExpr = (opts?.expression ?? "").trim();
26996
+ const optionsPresent = optsExpr.length > 0;
26997
+ const missingSecure = !SECURE_TRUE_RE.test(optsExpr);
26998
+ const missingHttpOnly = !HTTPONLY_TRUE_RE.test(optsExpr);
26999
+ if (!missingSecure && !missingHttpOnly) continue;
27000
+ const line = call.location.line;
27001
+ insecureCookies.push({
27002
+ line,
27003
+ receiver,
27004
+ missingSecure,
27005
+ missingHttpOnly,
27006
+ optionsPresent
27007
+ });
27008
+ const missing = [];
27009
+ if (missingSecure) missing.push("`secure: true`");
27010
+ if (missingHttpOnly) missing.push("`httpOnly: true`");
27011
+ ctx.addFinding({
27012
+ id: `${this.name}-${file}-${line}`,
27013
+ pass: this.name,
27014
+ category: this.category,
27015
+ rule_id: this.name,
27016
+ cwe: "CWE-614",
27017
+ severity: "medium",
27018
+ level: "warning",
27019
+ message: `Cookie set without ${missing.join(" and ")} \u2014 vulnerable to cleartext transmission (CWE-614) and client-side JS access (CWE-1004).`,
27020
+ file,
27021
+ line,
27022
+ fix: 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.',
27023
+ evidence: {
27024
+ receiver,
27025
+ options_present: optionsPresent,
27026
+ missing_secure: missingSecure,
27027
+ missing_http_only: missingHttpOnly
27028
+ }
27029
+ });
27030
+ }
27031
+ return { insecureCookies };
27032
+ }
27033
+ };
27034
+
26682
27035
  // src/analysis/metrics/passes/size-metrics-pass.ts
26683
27036
  var SizeMetricsPass = class {
26684
27037
  name = "size-metrics";
@@ -27575,6 +27928,7 @@ async function analyze(code, filePath, language, options = {}) {
27575
27928
  if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
27576
27929
  if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
27577
27930
  if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
27931
+ if (!disabledPasses.has("insecure-cookie")) pipeline.add(new InsecureCookiePass());
27578
27932
  const { results, findings } = pipeline.run(graph, code, language, config);
27579
27933
  const sinkFilter = results.get("sink-filter");
27580
27934
  const interProc = results.get("interprocedural");