circle-ir 3.46.0 → 3.48.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 (31) hide show
  1. package/dist/analysis/constant-propagation/propagator.d.ts +7 -0
  2. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  3. package/dist/analysis/constant-propagation/propagator.js +81 -41
  4. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  5. package/dist/analysis/html/html-attribute-security-pass.js +14 -9
  6. package/dist/analysis/html/html-attribute-security-pass.js.map +1 -1
  7. package/dist/analysis/html/html-extractor.d.ts.map +1 -1
  8. package/dist/analysis/html/html-extractor.js +16 -11
  9. package/dist/analysis/html/html-extractor.js.map +1 -1
  10. package/dist/analysis/passes/spring4shell-pass.d.ts +58 -0
  11. package/dist/analysis/passes/spring4shell-pass.d.ts.map +1 -0
  12. package/dist/analysis/passes/spring4shell-pass.js +252 -0
  13. package/dist/analysis/passes/spring4shell-pass.js.map +1 -0
  14. package/dist/analyzer.d.ts +1 -0
  15. package/dist/analyzer.d.ts.map +1 -1
  16. package/dist/analyzer.js +4 -0
  17. package/dist/analyzer.js.map +1 -1
  18. package/dist/browser/circle-ir.js +315 -73
  19. package/dist/core/circle-ir-core.cjs +62 -41
  20. package/dist/core/circle-ir-core.js +62 -41
  21. package/dist/core/parser.d.ts +10 -0
  22. package/dist/core/parser.d.ts.map +1 -1
  23. package/dist/core/parser.js +20 -5
  24. package/dist/core/parser.js.map +1 -1
  25. package/dist/languages/plugins/base.d.ts.map +1 -1
  26. package/dist/languages/plugins/base.js +15 -11
  27. package/dist/languages/plugins/base.js.map +1 -1
  28. package/dist/languages/plugins/java.d.ts.map +1 -1
  29. package/dist/languages/plugins/java.js +8 -4
  30. package/dist/languages/plugins/java.js.map +1 -1
  31. 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
  }
@@ -16032,8 +16034,10 @@ var ConstantPropagator = class _ConstantPropagator {
16032
16034
  * These are variables declared directly in the class body, not inside methods.
16033
16035
  */
16034
16036
  collectClassFields(root) {
16035
- const traverse = (n, inClass, inMethod) => {
16036
- if (!n) return;
16037
+ const stack = [root];
16038
+ while (stack.length > 0) {
16039
+ const n = stack.pop();
16040
+ if (!n) continue;
16037
16041
  if (n.type === "class_body") {
16038
16042
  for (const child of n.children) {
16039
16043
  if (child.type === "field_declaration") {
@@ -16047,32 +16051,28 @@ var ConstantPropagator = class _ConstantPropagator {
16047
16051
  }
16048
16052
  }
16049
16053
  }
16050
- if (child.type === "method_declaration" || child.type === "constructor_declaration") {
16051
- traverse(child, true, true);
16052
- } else {
16053
- traverse(child, true, false);
16054
- }
16054
+ stack.push(child);
16055
16055
  }
16056
- return;
16056
+ continue;
16057
16057
  }
16058
16058
  for (const child of n.children) {
16059
- traverse(child, inClass, inMethod);
16059
+ stack.push(child);
16060
16060
  }
16061
- };
16062
- traverse(root, false, false);
16061
+ }
16063
16062
  }
16064
16063
  findAllMethods(node) {
16065
16064
  const methods = [];
16066
- const traverse = (n) => {
16067
- if (!n) return;
16065
+ const stack = [node];
16066
+ while (stack.length > 0) {
16067
+ const n = stack.pop();
16068
+ if (!n) continue;
16068
16069
  if (n.type === "method_declaration" || n.type === "function_declaration") {
16069
16070
  methods.push(n);
16070
16071
  }
16071
16072
  for (const child of n.children) {
16072
- if (child) traverse(child);
16073
+ if (child) stack.push(child);
16073
16074
  }
16074
- };
16075
- traverse(node);
16075
+ }
16076
16076
  return methods;
16077
16077
  }
16078
16078
  getMethodName(method) {
@@ -16123,9 +16123,24 @@ var ConstantPropagator = class _ConstantPropagator {
16123
16123
  // AST Visitor
16124
16124
  // ===========================================================================
16125
16125
  visit(node) {
16126
+ const stack = [node];
16127
+ while (stack.length > 0) {
16128
+ const current = stack.pop();
16129
+ if (this.visitOne(current)) continue;
16130
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
16131
+ stack.push(current.children[i2]);
16132
+ }
16133
+ }
16134
+ }
16135
+ /**
16136
+ * Visit a single node. Returns true if the handler already descended into
16137
+ * children (and the caller should NOT push them), false to fall through to
16138
+ * the default pre-order descent.
16139
+ */
16140
+ visitOne(node) {
16126
16141
  const line = getNodeLine(node);
16127
16142
  if (this.unreachableLines.has(line)) {
16128
- return;
16143
+ return true;
16129
16144
  }
16130
16145
  if (this.conditionStack.length > 0 && !this.lineConditions.has(line)) {
16131
16146
  this.lineConditions.set(line, this.conditionStack[this.conditionStack.length - 1]);
@@ -16134,43 +16149,41 @@ var ConstantPropagator = class _ConstantPropagator {
16134
16149
  case "method_declaration":
16135
16150
  case "constructor_declaration":
16136
16151
  this.handleMethodDeclaration(node);
16137
- return;
16152
+ return true;
16138
16153
  // Don't visit children directly, handleMethodDeclaration does it
16139
16154
  case "local_variable_declaration":
16140
16155
  this.handleVariableDeclaration(node);
16141
- break;
16156
+ return false;
16142
16157
  case "assignment_expression":
16143
16158
  this.handleAssignment(node);
16144
- break;
16159
+ return false;
16145
16160
  case "update_expression":
16146
16161
  this.handleUpdateExpression(node);
16147
- break;
16162
+ return false;
16148
16163
  case "if_statement":
16149
16164
  this.handleIfStatement(node);
16150
- return;
16165
+ return true;
16151
16166
  case "switch_expression":
16152
16167
  case "switch_statement":
16153
16168
  this.handleSwitch(node);
16154
- return;
16169
+ return true;
16155
16170
  case "ternary_expression":
16156
16171
  this.handleTernary(node);
16157
- break;
16172
+ return false;
16158
16173
  case "expression_statement":
16159
16174
  this.handleExpressionStatement(node);
16160
- break;
16175
+ return false;
16161
16176
  case "for_statement":
16162
16177
  case "enhanced_for_statement":
16163
16178
  case "while_statement":
16164
16179
  case "do_statement":
16165
16180
  this.handleLoopStatement(node);
16166
- return;
16181
+ return true;
16167
16182
  case "synchronized_statement":
16168
16183
  this.handleSynchronizedStatement(node);
16169
- return;
16184
+ return true;
16170
16185
  default:
16171
- for (const child of node.children) {
16172
- this.visit(child);
16173
- }
16186
+ return false;
16174
16187
  }
16175
16188
  }
16176
16189
  /**
@@ -16938,6 +16951,19 @@ var ConstantPropagator = class _ConstantPropagator {
16938
16951
  return null;
16939
16952
  }
16940
16953
  isTaintedExpression(node) {
16954
+ const stack = [node];
16955
+ while (stack.length > 0) {
16956
+ const current = stack.pop();
16957
+ const result = this.isTaintedExpressionStep(current);
16958
+ if (result === true) return true;
16959
+ if (result === false) continue;
16960
+ for (let i2 = current.children.length - 1; i2 >= 0; i2--) {
16961
+ stack.push(current.children[i2]);
16962
+ }
16963
+ }
16964
+ return false;
16965
+ }
16966
+ isTaintedExpressionStep(node) {
16941
16967
  const text = getNodeText2(node, this.source);
16942
16968
  if (node.type === "method_invocation") {
16943
16969
  const nameNode = node.childForFieldName("name");
@@ -17197,12 +17223,7 @@ var ConstantPropagator = class _ConstantPropagator {
17197
17223
  }
17198
17224
  return isTainted;
17199
17225
  }
17200
- for (const child of node.children) {
17201
- if (this.isTaintedExpression(child)) {
17202
- return true;
17203
- }
17204
- }
17205
- return false;
17226
+ return void 0;
17206
17227
  }
17207
17228
  checkCollectionTaint(node) {
17208
17229
  const objectNode = node.childForFieldName("object");
@@ -17526,19 +17547,17 @@ var BaseLanguagePlugin = class {
17526
17547
  */
17527
17548
  findNodes(root, type) {
17528
17549
  const nodes = [];
17529
- const cursor = root.walk();
17530
- const visit = () => {
17531
- if (cursor.nodeType === type) {
17532
- nodes.push(cursor.currentNode);
17550
+ const stack = [root];
17551
+ while (stack.length > 0) {
17552
+ const node = stack.pop();
17553
+ if (node.type === type) {
17554
+ nodes.push(node);
17533
17555
  }
17534
- if (cursor.gotoFirstChild()) {
17535
- do {
17536
- visit();
17537
- } while (cursor.gotoNextSibling());
17538
- cursor.gotoParent();
17556
+ for (let i2 = node.childCount - 1; i2 >= 0; i2--) {
17557
+ const child = node.child(i2);
17558
+ if (child) stack.push(child);
17539
17559
  }
17540
- };
17541
- visit();
17560
+ }
17542
17561
  return nodes;
17543
17562
  }
17544
17563
  /**
@@ -17894,16 +17913,17 @@ var JavaPlugin = class extends BaseLanguagePlugin {
17894
17913
  }
17895
17914
  }
17896
17915
  };
17897
- const walk = (node) => {
17916
+ const stack = [tree.rootNode];
17917
+ while (stack.length > 0) {
17918
+ const node = stack.pop();
17898
17919
  if (node.type === "field_declaration" || node.type === "local_variable_declaration") {
17899
17920
  collectDecl(node);
17900
17921
  }
17901
17922
  for (let i2 = 0; i2 < node.childCount; i2++) {
17902
17923
  const child = node.child(i2);
17903
- if (child) walk(child);
17924
+ if (child) stack.push(child);
17904
17925
  }
17905
- };
17906
- walk(tree.rootNode);
17926
+ }
17907
17927
  this._typeMapCache.set(tree, map);
17908
17928
  return map;
17909
17929
  }
@@ -20700,16 +20720,18 @@ function extractHtmlContent(rootNode) {
20700
20720
  return { scriptBlocks, eventHandlers };
20701
20721
  }
20702
20722
  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);
20723
+ const stack = [node];
20724
+ while (stack.length > 0) {
20725
+ const current = stack.pop();
20726
+ if (current.type === "script_element") {
20727
+ extractScriptBlock(current, scriptBlocks);
20728
+ }
20729
+ if (current.type === "element" || current.type === "self_closing_tag") {
20730
+ extractEventHandlers(current, eventHandlers);
20731
+ }
20732
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
20733
+ const child = current.child(i2);
20734
+ if (child) stack.push(child);
20713
20735
  }
20714
20736
  }
20715
20737
  }
@@ -20806,13 +20828,15 @@ function runHtmlAttributeSecurityChecks(rootNode, filePath) {
20806
20828
  return findings;
20807
20829
  }
20808
20830
  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);
20831
+ const stack = [node];
20832
+ while (stack.length > 0) {
20833
+ const current = stack.pop();
20834
+ if (current.type === "element" || current.type === "self_closing_tag" || current.type === "script_element" || current.type === "style_element") {
20835
+ checkElement(current, filePath, findings);
20836
+ }
20837
+ for (let i2 = current.childCount - 1; i2 >= 0; i2--) {
20838
+ const child = current.child(i2);
20839
+ if (child) stack.push(child);
20816
20840
  }
20817
20841
  }
20818
20842
  }
@@ -26462,6 +26486,223 @@ var ScanSecretsPass = class {
26462
26486
  }
26463
26487
  };
26464
26488
 
26489
+ // src/analysis/passes/spring4shell-pass.ts
26490
+ var CONTROLLER_ANNOTATIONS = /* @__PURE__ */ new Set([
26491
+ "Controller",
26492
+ "RestController",
26493
+ "ControllerAdvice",
26494
+ "RestControllerAdvice"
26495
+ ]);
26496
+ var ROUTE_ANNOTATIONS = /* @__PURE__ */ new Set([
26497
+ "RequestMapping",
26498
+ "GetMapping",
26499
+ "PostMapping",
26500
+ "PutMapping",
26501
+ "DeleteMapping",
26502
+ "PatchMapping"
26503
+ ]);
26504
+ var BINDING_ANNOTATIONS = /* @__PURE__ */ new Set([
26505
+ "RequestBody",
26506
+ // JSON via Jackson (no DataBinder)
26507
+ "RequestParam",
26508
+ // scalar binding
26509
+ "PathVariable",
26510
+ // scalar binding
26511
+ "RequestHeader",
26512
+ // scalar binding
26513
+ "CookieValue",
26514
+ // scalar binding
26515
+ "MatrixVariable",
26516
+ // scalar binding
26517
+ "ModelAttribute",
26518
+ // explicit form binding — user opted in
26519
+ "Valid",
26520
+ // typically paired with @RequestBody / @ModelAttribute
26521
+ "Validated",
26522
+ "RequestPart",
26523
+ // multipart, explicit
26524
+ "SessionAttribute",
26525
+ "RequestAttribute"
26526
+ ]);
26527
+ var FRAMEWORK_PARAM_TYPES = /* @__PURE__ */ new Set([
26528
+ // Servlet / HTTP
26529
+ "HttpServletRequest",
26530
+ "HttpServletResponse",
26531
+ "ServletRequest",
26532
+ "ServletResponse",
26533
+ "HttpSession",
26534
+ "ServletContext",
26535
+ "Cookie",
26536
+ // Spring MVC plumbing
26537
+ "Model",
26538
+ "ModelMap",
26539
+ "ModelAndView",
26540
+ "Map",
26541
+ "BindingResult",
26542
+ "Errors",
26543
+ "RedirectAttributes",
26544
+ "SessionStatus",
26545
+ "WebRequest",
26546
+ "NativeWebRequest",
26547
+ "ServletWebRequest",
26548
+ "UriComponentsBuilder",
26549
+ "UriBuilder",
26550
+ "HttpEntity",
26551
+ "RequestEntity",
26552
+ "ResponseEntity",
26553
+ "HttpHeaders",
26554
+ "InputStream",
26555
+ "OutputStream",
26556
+ "Reader",
26557
+ "Writer",
26558
+ // Reactive
26559
+ "ServerHttpRequest",
26560
+ "ServerHttpResponse",
26561
+ "ServerWebExchange",
26562
+ // Security / locale
26563
+ "Principal",
26564
+ "Authentication",
26565
+ "Locale",
26566
+ "TimeZone",
26567
+ "ZoneId",
26568
+ // Multipart
26569
+ "MultipartFile",
26570
+ "Part",
26571
+ // Misc
26572
+ "TimeZone"
26573
+ ]);
26574
+ var SIMPLE_JAVA_TYPES = /* @__PURE__ */ new Set([
26575
+ // Primitives
26576
+ "boolean",
26577
+ "byte",
26578
+ "char",
26579
+ "short",
26580
+ "int",
26581
+ "long",
26582
+ "float",
26583
+ "double",
26584
+ "void",
26585
+ // Boxed primitives
26586
+ "Boolean",
26587
+ "Byte",
26588
+ "Character",
26589
+ "Short",
26590
+ "Integer",
26591
+ "Long",
26592
+ "Float",
26593
+ "Double",
26594
+ // Standard scalar-ish types
26595
+ "String",
26596
+ "CharSequence",
26597
+ "BigInteger",
26598
+ "BigDecimal",
26599
+ "UUID",
26600
+ // Date/time (Spring binds these via ConversionService, not DataBinder)
26601
+ "Date",
26602
+ "Calendar",
26603
+ "Instant",
26604
+ "LocalDate",
26605
+ "LocalTime",
26606
+ "LocalDateTime",
26607
+ "OffsetDateTime",
26608
+ "OffsetTime",
26609
+ "ZonedDateTime",
26610
+ "Duration",
26611
+ "Period",
26612
+ // Collections — first-class scalar list binding (?names=a&names=b)
26613
+ "List",
26614
+ "Set",
26615
+ "Collection",
26616
+ "Iterable",
26617
+ "Optional"
26618
+ // Arrays (handled by suffix check below)
26619
+ ]);
26620
+ var Spring4ShellPass = class {
26621
+ name = "spring4shell";
26622
+ category = "security";
26623
+ run(ctx) {
26624
+ const { graph, language } = ctx;
26625
+ if (language !== "java") {
26626
+ return { controllerMethodsScanned: 0, findingsEmitted: 0 };
26627
+ }
26628
+ const file = graph.ir.meta.file;
26629
+ let scanned = 0;
26630
+ let emitted = 0;
26631
+ for (const type of graph.ir.types) {
26632
+ if (!isController(type)) continue;
26633
+ for (const method of type.methods) {
26634
+ if (!isRouteHandler(method)) continue;
26635
+ scanned++;
26636
+ for (const param of method.parameters) {
26637
+ if (!isVulnerableParameter(param)) continue;
26638
+ ctx.addFinding({
26639
+ id: `${this.name}-${file}-${method.start_line}-${param.name}`,
26640
+ pass: this.name,
26641
+ category: this.category,
26642
+ rule_id: this.name,
26643
+ cwe: "CWE-94",
26644
+ severity: "high",
26645
+ level: "error",
26646
+ message: `Spring MVC controller method '${type.name}.${method.name}' binds parameter '${param.name}' of type '${param.type ?? "?"}' via implicit form-data binding (no @RequestBody / @RequestParam / @ModelAttribute) \u2014 vulnerable to Spring4Shell (CVE-2022-22965) class-graph RCE on Spring < 5.3.18 / 5.2.20`,
26647
+ file,
26648
+ line: param.line ?? method.start_line,
26649
+ fix: "Annotate the parameter with @RequestBody (JSON) or @ModelAttribute + @InitBinder/setAllowedFields whitelisting, upgrade Spring to \u2265 5.3.18 / 5.2.20, and ensure JDK is patched.",
26650
+ evidence: {
26651
+ controller_class: type.name,
26652
+ controller_annotations: type.annotations,
26653
+ method: method.name,
26654
+ method_annotations: method.annotations,
26655
+ parameter_name: param.name,
26656
+ parameter_type: param.type
26657
+ }
26658
+ });
26659
+ emitted++;
26660
+ }
26661
+ }
26662
+ }
26663
+ return { controllerMethodsScanned: scanned, findingsEmitted: emitted };
26664
+ }
26665
+ };
26666
+ function annotationHead(annotation) {
26667
+ const parenIdx = annotation.indexOf("(");
26668
+ return parenIdx >= 0 ? annotation.slice(0, parenIdx) : annotation;
26669
+ }
26670
+ function hasAnnotation(annotations, names) {
26671
+ for (const a of annotations) {
26672
+ if (names.has(annotationHead(a))) return true;
26673
+ }
26674
+ return false;
26675
+ }
26676
+ function isController(type) {
26677
+ return hasAnnotation(type.annotations, CONTROLLER_ANNOTATIONS);
26678
+ }
26679
+ function isRouteHandler(method) {
26680
+ return hasAnnotation(method.annotations, ROUTE_ANNOTATIONS);
26681
+ }
26682
+ function isVulnerableParameter(param) {
26683
+ if (hasAnnotation(param.annotations, BINDING_ANNOTATIONS)) return false;
26684
+ if (!param.type) return false;
26685
+ const type = stripGenerics2(param.type).trim();
26686
+ if (!type) return false;
26687
+ if (type.endsWith("[]")) {
26688
+ const elem = type.slice(0, -2).trim();
26689
+ return !SIMPLE_JAVA_TYPES.has(elem) && isPotentialPojo(elem);
26690
+ }
26691
+ if (SIMPLE_JAVA_TYPES.has(type)) return false;
26692
+ if (FRAMEWORK_PARAM_TYPES.has(type)) return false;
26693
+ if (!isPotentialPojo(type)) return false;
26694
+ return true;
26695
+ }
26696
+ function stripGenerics2(type) {
26697
+ const ltIdx = type.indexOf("<");
26698
+ return ltIdx >= 0 ? type.slice(0, ltIdx) : type;
26699
+ }
26700
+ function isPotentialPojo(type) {
26701
+ if (type.length === 0) return false;
26702
+ const first = type.charCodeAt(0);
26703
+ return first >= 65 && first <= 90;
26704
+ }
26705
+
26465
26706
  // src/analysis/metrics/passes/size-metrics-pass.ts
26466
26707
  var SizeMetricsPass = class {
26467
26708
  name = "size-metrics";
@@ -27357,6 +27598,7 @@ async function analyze(code, filePath, language, options = {}) {
27357
27598
  if (!disabledPasses.has("god-class")) pipeline.add(new GodClassPass());
27358
27599
  if (!disabledPasses.has("naming-convention")) pipeline.add(new NamingConventionPass(passOpts.namingConvention));
27359
27600
  if (!disabledPasses.has("security-headers")) pipeline.add(new SecurityHeadersPass(passOpts.securityHeaders));
27601
+ if (!disabledPasses.has("spring4shell")) pipeline.add(new Spring4ShellPass());
27360
27602
  const { results, findings } = pipeline.run(graph, code, language, config);
27361
27603
  const sinkFilter = results.get("sink-filter");
27362
27604
  const interProc = results.get("interprocedural");