circle-ir 3.152.0 → 3.154.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 (34) hide show
  1. package/configs/sink-semantics.json +102 -0
  2. package/configs/sinks/command.yaml +0 -10
  3. package/configs/sinks/deserialization.yaml +54 -0
  4. package/configs/sinks/path.yaml +0 -94
  5. package/configs/sinks/sql.yaml +24 -24
  6. package/dist/analysis/config-loader.d.ts.map +1 -1
  7. package/dist/analysis/config-loader.js +24 -8
  8. package/dist/analysis/config-loader.js.map +1 -1
  9. package/dist/analysis/passes/library-profile-sink-gate-pass.d.ts +43 -1
  10. package/dist/analysis/passes/library-profile-sink-gate-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/library-profile-sink-gate-pass.js +75 -0
  12. package/dist/analysis/passes/library-profile-sink-gate-pass.js.map +1 -1
  13. package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts +104 -0
  14. package/dist/analysis/passes/library-profile-xss-gate-pass.d.ts.map +1 -0
  15. package/dist/analysis/passes/library-profile-xss-gate-pass.js +196 -0
  16. package/dist/analysis/passes/library-profile-xss-gate-pass.js.map +1 -0
  17. package/dist/analysis/require-entry-path.d.ts +91 -0
  18. package/dist/analysis/require-entry-path.d.ts.map +1 -0
  19. package/dist/analysis/require-entry-path.js +387 -0
  20. package/dist/analysis/require-entry-path.js.map +1 -0
  21. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  22. package/dist/analysis/taint-matcher.js +55 -1
  23. package/dist/analysis/taint-matcher.js.map +1 -1
  24. package/dist/analyzer.d.ts.map +1 -1
  25. package/dist/analyzer.js +37 -1
  26. package/dist/analyzer.js.map +1 -1
  27. package/dist/browser/circle-ir.js +437 -257
  28. package/dist/core/circle-ir-core.cjs +38 -9
  29. package/dist/core/circle-ir-core.js +38 -9
  30. package/dist/types/config.d.ts +11 -1
  31. package/dist/types/config.d.ts.map +1 -1
  32. package/dist/types/index.d.ts +25 -0
  33. package/dist/types/index.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -11286,11 +11286,11 @@ var DEFAULT_SINKS = [
11286
11286
  { method: "FileOutputStream", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11287
11287
  { method: "FileReader", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11288
11288
  { method: "FileWriter", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11289
- // ClassLoader/Class resource loading (can be abused for path traversal)
11290
- { method: "getResource", class: "ClassLoader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11291
- { method: "getResourceAsStream", class: "ClassLoader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11292
- { method: "getResource", class: "Class", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11293
- { method: "getResourceAsStream", class: "Class", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11289
+ // NOTE: ClassLoader.getResource / Class.getResource were removed in
11290
+ // 3.153.0 (#233). Classpath resource resolution cannot escape the
11291
+ // classpath root via `../` (JAR entries are opaque). If reintroduced,
11292
+ // the correct CWE is CWE-829 (untrusted-classpath-resource), not
11293
+ // CWE-22 path traversal.
11294
11294
  // Paths.get can be used for path traversal
11295
11295
  { method: "get", class: "Paths", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11296
11296
  { method: "of", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -11304,9 +11304,16 @@ var DEFAULT_SINKS = [
11304
11304
  { method: "newBufferedWriter", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11305
11305
  { method: "copy", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
11306
11306
  { method: "move", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0, 1] },
11307
- { method: "exists", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11308
- { method: "isDirectory", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11309
- { method: "isRegularFile", class: "Files", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11307
+ // NOTE: `Files.exists`, `Files.isDirectory`, `Files.isRegularFile`
11308
+ // were removed in 3.154.0 (#245 RC2). These NIO methods are pure
11309
+ // boolean queries they read a filesystem attribute and cannot
11310
+ // cause traversal escape. A CWE-22 sink must consume the path to
11311
+ // open, read, write, delete, list, or link a filesystem entry;
11312
+ // check-only receivers reveal at most a boolean. Empirically
11313
+ // ~12 H+C FPs across the 10-repo Tier 2 cohort
11314
+ // (cognium-ai#189 §4). `java.io.File` instance methods
11315
+ // (`file.isDirectory()`, `file.exists()`, `file.canRead()`, …)
11316
+ // are already not registered as CWE-22 sinks.
11310
11317
  // RandomAccessFile
11311
11318
  { method: "RandomAccessFile", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11312
11319
  // Framework-specific resource loading (Cocoon, Spring, etc.)
@@ -11793,6 +11800,15 @@ var DEFAULT_SINKS = [
11793
11800
  { method: "parseObject", class: "JSON", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], safe_if_class_literal_at: 1 },
11794
11801
  { method: "parseObject", class: "JSONObject", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], safe_if_class_literal_at: 1 },
11795
11802
  { method: "fromJson", class: "Gson", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0], safe_if_class_literal_at: 1 },
11803
+ // Jackson ObjectReader — pre-configured reader; typed 2-arg overload is safe
11804
+ // when arg[1] is a class literal or TypeReference<>()/TypeToken<>() (handled
11805
+ // by argIsClassLiteral extension). Added 3.153.0 (cognium-dev #233).
11806
+ { method: "readValue", class: "ObjectReader", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], safe_if_class_literal_at: 1 },
11807
+ // Jackson ObjectMapper.convertValue — same class-literal shape as readValue.
11808
+ { method: "convertValue", class: "ObjectMapper", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0], safe_if_class_literal_at: 1 },
11809
+ // Kryo.readObject(input, User.class) — typed form is safe; polymorphic form
11810
+ // (Kryo.readClassAndObject or non-literal type) remains a sink.
11811
+ { method: "readObject", class: "Kryo", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0], safe_if_class_literal_at: 1 },
11796
11812
  // XMLDecoder
11797
11813
  { method: "readObject", class: "XMLDecoder", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [] },
11798
11814
  // Java serialization constructors
@@ -13652,7 +13668,17 @@ function argIsClassLiteral(call, position) {
13652
13668
  if (!arg) return false;
13653
13669
  const expr = (arg.literal ?? arg.expression ?? "").trim();
13654
13670
  if (!expr) return false;
13655
- return CLASS_LITERAL_RE.test(expr);
13671
+ if (CLASS_LITERAL_RE.test(expr)) return true;
13672
+ return TYPE_TOKEN_RE.test(expr);
13673
+ }
13674
+ var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
13675
+ function argIsStringLiteral(call, position) {
13676
+ const arg = call.arguments.find((a) => a.position === position);
13677
+ if (!arg) return false;
13678
+ if (arg.literal !== void 0 && arg.literal !== null && arg.literal !== "") return true;
13679
+ const expr = (arg.expression ?? "").trim();
13680
+ if (!expr) return false;
13681
+ return expr.startsWith('"') && expr.endsWith('"') && expr.length >= 2 || expr.startsWith("'") && expr.endsWith("'") && expr.length >= 2 || expr.startsWith('"""') && expr.endsWith('"""');
13656
13682
  }
13657
13683
  var CWE_78_RECEIVER_ALLOWLIST = /* @__PURE__ */ new Set([
13658
13684
  // java.lang.*
@@ -13827,6 +13853,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
13827
13853
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
13828
13854
  continue;
13829
13855
  }
13856
+ if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
13857
+ continue;
13858
+ }
13830
13859
  if (pattern.type === "command_injection") {
13831
13860
  if (call.is_constructor) {
13832
13861
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -19553,6 +19582,238 @@ function applyLibraryApiSurfaceDowngrade(findings) {
19553
19582
  });
19554
19583
  }
19555
19584
 
19585
+ // src/analysis/entry-point-detection.ts
19586
+ var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
19587
+ // Spring MVC
19588
+ "RequestMapping",
19589
+ "GetMapping",
19590
+ "PostMapping",
19591
+ "PutMapping",
19592
+ "DeleteMapping",
19593
+ "PatchMapping",
19594
+ // Spring messaging / WebSocket
19595
+ "MessageMapping",
19596
+ "SubscribeMapping",
19597
+ // Spring messaging listeners
19598
+ "KafkaListener",
19599
+ "KafkaHandler",
19600
+ "RabbitListener",
19601
+ "RabbitHandler",
19602
+ "JmsListener",
19603
+ "StreamListener",
19604
+ // Spring Cloud AWS
19605
+ "SqsListener",
19606
+ "SqsHandler",
19607
+ // Spring application events (borderline — inclusive in ship 1)
19608
+ "EventListener",
19609
+ // CRON / scheduled
19610
+ "Scheduled",
19611
+ // JAX-RS
19612
+ "Path",
19613
+ "GET",
19614
+ "POST",
19615
+ "PUT",
19616
+ "DELETE",
19617
+ "PATCH",
19618
+ "HEAD",
19619
+ "OPTIONS",
19620
+ // Jenkins Stapler form-binding (#224 — CVE-2022-20617 docker-commons).
19621
+ // `@DataBoundConstructor` / `@DataBoundSetter` mark the trust boundary
19622
+ // between Jenkins UI (or `config.xml` unmarshal) and the plugin: the
19623
+ // Stapler runtime invokes the annotated constructor / setter with
19624
+ // user-supplied strings from a submitted form or a persisted job
19625
+ // config. Every parameter is a user-supplied taint source.
19626
+ "DataBoundConstructor",
19627
+ "DataBoundSetter"
19628
+ ]);
19629
+ var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
19630
+ // Spring MVC
19631
+ "RestController",
19632
+ "Controller",
19633
+ // Spring stereotype beans (#136 — see header)
19634
+ "Service",
19635
+ "Repository",
19636
+ "Component",
19637
+ // JAX-RS resource class
19638
+ "Path",
19639
+ // Servlet 3.0 annotation-based servlet
19640
+ "WebServlet",
19641
+ // JSR-356 WebSocket endpoint
19642
+ "ServerEndpoint",
19643
+ // Declarative HTTP client (borderline — inclusive in ship 1; contract-defined
19644
+ // parameter shapes are downstream-relevant trust boundaries)
19645
+ "FeignClient"
19646
+ ]);
19647
+ var TIER_1_BY_SUPERTYPE = /* @__PURE__ */ new Map([
19648
+ // Servlet API
19649
+ ["HttpServlet", /* @__PURE__ */ new Set(["doGet", "doPost", "doPut", "doDelete", "doHead", "doOptions", "doTrace", "service"])],
19650
+ ["GenericServlet", /* @__PURE__ */ new Set(["service"])],
19651
+ ["Filter", /* @__PURE__ */ new Set(["doFilter"])],
19652
+ // Spring web
19653
+ ["HandlerInterceptor", /* @__PURE__ */ new Set(["preHandle", "postHandle", "afterCompletion"])],
19654
+ ["AsyncHandlerInterceptor", /* @__PURE__ */ new Set(["preHandle", "postHandle", "afterCompletion", "afterConcurrentHandlingStarted"])],
19655
+ // Spring boot CLI entry points
19656
+ ["CommandLineRunner", /* @__PURE__ */ new Set(["run"])],
19657
+ ["ApplicationRunner", /* @__PURE__ */ new Set(["run"])],
19658
+ // Netty channel handlers (#154 — CVE-2022-26884 dolphinscheduler).
19659
+ // `SimpleChannelInboundHandler<T>.channelRead0(ctx, msg)` is the
19660
+ // standard typed read entry; `ChannelInboundHandler.channelRead(ctx,
19661
+ // Object)` is the untyped base. `ChannelInboundHandlerAdapter` and
19662
+ // `ChannelDuplexHandler` are the most-common adapter classes user
19663
+ // handlers extend. `NettyRequestProcessor` is the dolphinscheduler-
19664
+ // specific (and other Netty-RPC projects') wire-message processor
19665
+ // surface — `process(channel, command)` is the network entry point.
19666
+ ["SimpleChannelInboundHandler", /* @__PURE__ */ new Set(["channelRead0", "messageReceived"])],
19667
+ ["ChannelInboundHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
19668
+ ["ChannelInboundHandlerAdapter", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
19669
+ ["ChannelDuplexHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
19670
+ ["NettyRequestProcessor", /* @__PURE__ */ new Set(["process"])],
19671
+ // XStream deserialization converters (#224 — CVE-2020-26217,
19672
+ // CVE-2021-21345). The xstream deserializer invokes `unmarshal`
19673
+ // with attacker-controlled `HierarchicalStreamReader` state whenever
19674
+ // untrusted XML reaches `XStream.fromXML`. Each converter is a
19675
+ // deserialization gadget surface — its `unmarshal` parameters are
19676
+ // network-facing taint sources even though the enclosing class
19677
+ // carries no framework annotation. `marshal` is included for the
19678
+ // symmetric round-trip surface but is rarely exploitable on its
19679
+ // own; the fixup targets `unmarshal` primarily.
19680
+ //
19681
+ // `SingleValueConverter` is the string-form variant (fromString
19682
+ // parses an attacker string; toString is the sink half of the
19683
+ // round-trip). `ConverterMatcher` is the shared base and some
19684
+ // downstream projects subclass it directly.
19685
+ ["Converter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
19686
+ ["SingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
19687
+ ["ConverterMatcher", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
19688
+ // XStream abstract base classes — direct-parent `extends` match
19689
+ // covers the common shape where user converters subclass a base
19690
+ // rather than implementing `Converter` directly.
19691
+ ["AbstractReflectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal", "doMarshal", "doUnmarshal"])],
19692
+ ["AbstractSingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
19693
+ ["AbstractCollectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])]
19694
+ ]);
19695
+ var TIER_3_CLASS_SUFFIXES = [
19696
+ "Util",
19697
+ "Utils",
19698
+ "Helper",
19699
+ "Helpers"
19700
+ ];
19701
+ var TIER_3_PACKAGE_FRAGMENTS = [
19702
+ ".template.",
19703
+ ".templates.",
19704
+ ".engine.",
19705
+ ".engines."
19706
+ ];
19707
+ var TIER_3_JDK_FACADE_INTERFACES = /* @__PURE__ */ new Set([
19708
+ // Collection root + common containers
19709
+ "Collection",
19710
+ "List",
19711
+ "Set",
19712
+ "Map",
19713
+ "Queue",
19714
+ "Deque",
19715
+ "SortedSet",
19716
+ "SortedMap",
19717
+ "NavigableSet",
19718
+ "NavigableMap",
19719
+ // Iteration / ordering / equality contracts
19720
+ "Iterator",
19721
+ "Iterable",
19722
+ "ListIterator",
19723
+ "Comparator",
19724
+ "Comparable",
19725
+ // Serialization / cloning contracts
19726
+ "Serializable",
19727
+ "Externalizable",
19728
+ "Cloneable"
19729
+ ]);
19730
+ function classNameLooksLikeUtility(name2) {
19731
+ if (!name2) return false;
19732
+ for (const suffix of TIER_3_CLASS_SUFFIXES) {
19733
+ if (name2.length > suffix.length && name2.endsWith(suffix)) return true;
19734
+ }
19735
+ return false;
19736
+ }
19737
+ function packageLooksLikeTemplateOrEngine(pkg) {
19738
+ if (!pkg) return false;
19739
+ const padded = `.${pkg}.`;
19740
+ for (const frag of TIER_3_PACKAGE_FRAGMENTS) {
19741
+ if (padded.includes(frag)) return true;
19742
+ }
19743
+ return false;
19744
+ }
19745
+ function implementsJdkFacade(t) {
19746
+ if (!t) return false;
19747
+ for (const impl of t.implements ?? []) {
19748
+ if (TIER_3_JDK_FACADE_INTERFACES.has(simpleTypeName(impl))) return true;
19749
+ }
19750
+ return false;
19751
+ }
19752
+ function classShapeIsLibraryFacade(enclosingType) {
19753
+ if (!enclosingType) return false;
19754
+ if (classNameLooksLikeUtility(enclosingType.name)) return true;
19755
+ if (packageLooksLikeTemplateOrEngine(enclosingType.package)) return true;
19756
+ if (implementsJdkFacade(enclosingType)) return true;
19757
+ return false;
19758
+ }
19759
+ function annotationsInclude(annotations, targets) {
19760
+ if (!annotations || annotations.length === 0) return false;
19761
+ for (const raw of annotations) {
19762
+ const simple = raw.replace(/^@/, "").replace(/[<(].*$/, "").trim();
19763
+ if (targets.has(simple)) return true;
19764
+ }
19765
+ return false;
19766
+ }
19767
+ function simpleTypeName(ref) {
19768
+ return ref.replace(/<.*$/, "").trim();
19769
+ }
19770
+ function looksLikeMainMethod(method) {
19771
+ if (method.name !== "main") return false;
19772
+ const params = method.parameters ?? [];
19773
+ if (params.length !== 1) return false;
19774
+ const t = (params[0].type ?? "").replace(/\s+/g, "");
19775
+ return t === "String[]" || t === "String..." || t === "java.lang.String[]";
19776
+ }
19777
+ function methodIsSupertypeLifecycleEntryPoint(method, enclosingType) {
19778
+ if (!method.name) return false;
19779
+ if (!enclosingType) return false;
19780
+ const candidates = [];
19781
+ if (enclosingType.extends) candidates.push(simpleTypeName(enclosingType.extends));
19782
+ for (const i2 of enclosingType.implements ?? []) candidates.push(simpleTypeName(i2));
19783
+ for (const supertype of candidates) {
19784
+ const lifecycleMethods = TIER_1_BY_SUPERTYPE.get(supertype);
19785
+ if (lifecycleMethods?.has(method.name)) return true;
19786
+ }
19787
+ return false;
19788
+ }
19789
+ function classifyEntryPointTier(method, enclosingType, ctx) {
19790
+ const language = (ctx.language ?? "").toLowerCase();
19791
+ if (language !== "java") return "TIER_UNKNOWN";
19792
+ if (!method) return "TIER_UNKNOWN";
19793
+ if (classShapeIsLibraryFacade(enclosingType)) {
19794
+ return "TIER_3_LIBRARY_API";
19795
+ }
19796
+ if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS)) {
19797
+ return "TIER_1_ENTRY_POINT";
19798
+ }
19799
+ if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS)) {
19800
+ return "TIER_1_ENTRY_POINT";
19801
+ }
19802
+ if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
19803
+ return "TIER_1_ENTRY_POINT";
19804
+ }
19805
+ if (looksLikeMainMethod(method)) {
19806
+ return "TIER_1_ENTRY_POINT";
19807
+ }
19808
+ return "TIER_3_LIBRARY_API";
19809
+ }
19810
+ function shouldGateInterproceduralParam(sourceType, enclosingMethod, enclosingType, ctx) {
19811
+ if (sourceType !== "interprocedural_param") return false;
19812
+ if (!enclosingMethod) return false;
19813
+ const tier = classifyEntryPointTier(enclosingMethod, enclosingType, ctx);
19814
+ return tier === "TIER_3_LIBRARY_API";
19815
+ }
19816
+
19556
19817
  // src/analysis/project-profile-transform.ts
19557
19818
  var DOWNGRADE_ELIGIBLE_RULE_IDS = /* @__PURE__ */ new Set([
19558
19819
  "code_injection",
@@ -30818,7 +31079,7 @@ var REFLECTION_SINK_METHODS = /* @__PURE__ */ new Set([
30818
31079
  "loadClass",
30819
31080
  "defineClass"
30820
31081
  ]);
30821
- var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
31082
+ var TIER_1_CLASS_ANNOTATIONS2 = /* @__PURE__ */ new Set([
30822
31083
  // Spring MVC
30823
31084
  "RestController",
30824
31085
  "Controller",
@@ -30835,7 +31096,7 @@ var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
30835
31096
  // Declarative HTTP client
30836
31097
  "FeignClient"
30837
31098
  ]);
30838
- var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
31099
+ var TIER_1_METHOD_ANNOTATIONS2 = /* @__PURE__ */ new Set([
30839
31100
  // Spring MVC
30840
31101
  "RequestMapping",
30841
31102
  "GetMapping",
@@ -30936,7 +31197,7 @@ var CliMainReflectionSuppressPass = class {
30936
31197
  let hasFrameworkSignal = false;
30937
31198
  for (const type of types) {
30938
31199
  for (const ann of type.annotations) {
30939
- if (TIER_1_CLASS_ANNOTATIONS.has(normalizeAnnotation(ann))) {
31200
+ if (TIER_1_CLASS_ANNOTATIONS2.has(normalizeAnnotation(ann))) {
30940
31201
  hasFrameworkSignal = true;
30941
31202
  break;
30942
31203
  }
@@ -30955,7 +31216,7 @@ var CliMainReflectionSuppressPass = class {
30955
31216
  if (hasFrameworkSignal) break;
30956
31217
  for (const method of type.methods) {
30957
31218
  for (const ann of method.annotations) {
30958
- if (TIER_1_METHOD_ANNOTATIONS.has(normalizeAnnotation(ann))) {
31219
+ if (TIER_1_METHOD_ANNOTATIONS2.has(normalizeAnnotation(ann))) {
30959
31220
  hasFrameworkSignal = true;
30960
31221
  break;
30961
31222
  }
@@ -31044,21 +31305,168 @@ var LibraryProfileSinkGatePass = class {
31044
31305
  };
31045
31306
  }
31046
31307
  };
31047
-
31048
- // src/analysis/passes/taint-propagation-pass.ts
31049
- var TaintPropagationPass = class {
31050
- name = "taint-propagation";
31308
+ var CWE22_SPECULATIVE_SOURCE_TYPES = /* @__PURE__ */ new Set([
31309
+ "interprocedural_param",
31310
+ "constructor_field"
31311
+ ]);
31312
+ var LibraryProfileCwe22PathGatePass = class {
31313
+ name = "library-profile-cwe22-path-gate";
31051
31314
  category = "security";
31052
31315
  run(ctx) {
31053
31316
  const { graph } = ctx;
31054
- const { calls, types } = graph.ir;
31055
- const constProp = ctx.getResult("constant-propagation");
31056
- const sinkFilter = ctx.getResult("sink-filter");
31057
- const { sources, sinks, sanitizers } = sinkFilter;
31058
- if (sinks.length === 0) {
31059
- return { flows: [] };
31060
- }
31061
- const canSynthesize = ctx.language === "python" && typeof ctx.code === "string";
31317
+ const profile = graph.ir.meta.projectProfile;
31318
+ if (!isLibraryShape2(profile)) {
31319
+ return {
31320
+ profile,
31321
+ applied: false,
31322
+ dropped: 0,
31323
+ droppedBySourceType: {}
31324
+ };
31325
+ }
31326
+ const flows = graph.ir.taint.flows;
31327
+ if (!flows || flows.length === 0) {
31328
+ return {
31329
+ profile,
31330
+ applied: true,
31331
+ dropped: 0,
31332
+ droppedBySourceType: {}
31333
+ };
31334
+ }
31335
+ const droppedBySourceType = {};
31336
+ const kept = [];
31337
+ for (const flow of flows) {
31338
+ if (flow.sink_type === "path_traversal" && CWE22_SPECULATIVE_SOURCE_TYPES.has(flow.source_type)) {
31339
+ droppedBySourceType[flow.source_type] = (droppedBySourceType[flow.source_type] ?? 0) + 1;
31340
+ continue;
31341
+ }
31342
+ kept.push(flow);
31343
+ }
31344
+ const dropped = flows.length - kept.length;
31345
+ if (dropped > 0) {
31346
+ flows.length = 0;
31347
+ flows.push(...kept);
31348
+ }
31349
+ return {
31350
+ profile,
31351
+ applied: true,
31352
+ dropped,
31353
+ droppedBySourceType
31354
+ };
31355
+ }
31356
+ };
31357
+
31358
+ // src/analysis/passes/library-profile-xss-gate-pass.ts
31359
+ var XSS_NON_HTML_OUTPUT_CLASSES = /* @__PURE__ */ new Set([
31360
+ // In-memory buffers — no HTML output surface.
31361
+ "StringBuilder",
31362
+ "StringBuffer",
31363
+ "CharArrayWriter",
31364
+ "ByteArrayOutputStream",
31365
+ // CLI stdio — CLI apps, not web response bodies.
31366
+ "PrintStream",
31367
+ "System",
31368
+ // HTTP client builders — these are taint SOURCES (outbound reads),
31369
+ // not XSS sinks. `response.body()`, `.post()`, `.get()` on
31370
+ // hutool `HttpRequest` / `HttpResponse` reach us as sink
31371
+ // matches only because xss.yaml catches String-valued receivers.
31372
+ "HttpRequest",
31373
+ "HttpRequestBuilder",
31374
+ "HttpResponse",
31375
+ // Servlet non-body IO. `HttpSession.setAttribute`, `HttpSession.putValue`,
31376
+ // `HttpServletRequest.setAttribute` are session/request attribute
31377
+ // IO, not HTML output. Under `library/*` there is no JSP renderer
31378
+ // reflecting them back. `HttpServletResponse` itself is NOT on
31379
+ // this list — its writers are genuine XSS sinks.
31380
+ "HttpSession",
31381
+ "ServletRequest",
31382
+ "HttpServletRequest",
31383
+ // Wire-protocol writers (jedis internal).
31384
+ "RedisOutputStream",
31385
+ "SafeEncoder",
31386
+ "RESP2",
31387
+ "Protocol",
31388
+ // JSON parsers — these read JSON into POJOs. Source, not sink.
31389
+ "JSONUtil",
31390
+ "JSON",
31391
+ "ObjectMapper",
31392
+ "JsonReader",
31393
+ // Loggers — log injection is CWE-117 (already covered by #112);
31394
+ // these appear here because xss.yaml has a String-valued catch-all
31395
+ // that hits Logger receivers.
31396
+ "Logger",
31397
+ "LoggerFactory",
31398
+ "Log",
31399
+ "Slf4jLogger",
31400
+ // Router / interceptor context stores. Zuul `RequestContext`,
31401
+ // Sentinel `Context` are internal request-processing state, not
31402
+ // HTML output.
31403
+ "RequestContext",
31404
+ "Context"
31405
+ ]);
31406
+ function isLibraryShape3(profile) {
31407
+ if (!profile || profile === "unknown") return false;
31408
+ return profile.startsWith("library/");
31409
+ }
31410
+ var LibraryProfileXssGatePass = class {
31411
+ name = "library-profile-xss-gate";
31412
+ category = "security";
31413
+ run(ctx) {
31414
+ const { graph } = ctx;
31415
+ const profile = graph.ir.meta.projectProfile;
31416
+ if (!isLibraryShape3(profile)) {
31417
+ return {
31418
+ profile,
31419
+ applied: false,
31420
+ dropped: 0,
31421
+ droppedByClass: {}
31422
+ };
31423
+ }
31424
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
31425
+ if (sinks.length === 0) {
31426
+ return {
31427
+ profile,
31428
+ applied: true,
31429
+ dropped: 0,
31430
+ droppedByClass: {}
31431
+ };
31432
+ }
31433
+ const droppedByClass = {};
31434
+ const kept = [];
31435
+ for (const sink of sinks) {
31436
+ if (sink.type === "xss" && sink.class && XSS_NON_HTML_OUTPUT_CLASSES.has(sink.class)) {
31437
+ droppedByClass[sink.class] = (droppedByClass[sink.class] ?? 0) + 1;
31438
+ continue;
31439
+ }
31440
+ kept.push(sink);
31441
+ }
31442
+ const dropped = sinks.length - kept.length;
31443
+ if (dropped > 0) {
31444
+ sinks.length = 0;
31445
+ sinks.push(...kept);
31446
+ }
31447
+ return {
31448
+ profile,
31449
+ applied: true,
31450
+ dropped,
31451
+ droppedByClass
31452
+ };
31453
+ }
31454
+ };
31455
+
31456
+ // src/analysis/passes/taint-propagation-pass.ts
31457
+ var TaintPropagationPass = class {
31458
+ name = "taint-propagation";
31459
+ category = "security";
31460
+ run(ctx) {
31461
+ const { graph } = ctx;
31462
+ const { calls, types } = graph.ir;
31463
+ const constProp = ctx.getResult("constant-propagation");
31464
+ const sinkFilter = ctx.getResult("sink-filter");
31465
+ const { sources, sinks, sanitizers } = sinkFilter;
31466
+ if (sinks.length === 0) {
31467
+ return { flows: [] };
31468
+ }
31469
+ const canSynthesize = ctx.language === "python" && typeof ctx.code === "string";
31062
31470
  if (sources.length === 0 && !canSynthesize) {
31063
31471
  return { flows: [] };
31064
31472
  }
@@ -31835,238 +32243,6 @@ function pickScopedSource(sources, sinkLine, methodName, types, taintedVar) {
31835
32243
  return sources[0];
31836
32244
  }
31837
32245
 
31838
- // src/analysis/entry-point-detection.ts
31839
- var TIER_1_METHOD_ANNOTATIONS2 = /* @__PURE__ */ new Set([
31840
- // Spring MVC
31841
- "RequestMapping",
31842
- "GetMapping",
31843
- "PostMapping",
31844
- "PutMapping",
31845
- "DeleteMapping",
31846
- "PatchMapping",
31847
- // Spring messaging / WebSocket
31848
- "MessageMapping",
31849
- "SubscribeMapping",
31850
- // Spring messaging listeners
31851
- "KafkaListener",
31852
- "KafkaHandler",
31853
- "RabbitListener",
31854
- "RabbitHandler",
31855
- "JmsListener",
31856
- "StreamListener",
31857
- // Spring Cloud AWS
31858
- "SqsListener",
31859
- "SqsHandler",
31860
- // Spring application events (borderline — inclusive in ship 1)
31861
- "EventListener",
31862
- // CRON / scheduled
31863
- "Scheduled",
31864
- // JAX-RS
31865
- "Path",
31866
- "GET",
31867
- "POST",
31868
- "PUT",
31869
- "DELETE",
31870
- "PATCH",
31871
- "HEAD",
31872
- "OPTIONS",
31873
- // Jenkins Stapler form-binding (#224 — CVE-2022-20617 docker-commons).
31874
- // `@DataBoundConstructor` / `@DataBoundSetter` mark the trust boundary
31875
- // between Jenkins UI (or `config.xml` unmarshal) and the plugin: the
31876
- // Stapler runtime invokes the annotated constructor / setter with
31877
- // user-supplied strings from a submitted form or a persisted job
31878
- // config. Every parameter is a user-supplied taint source.
31879
- "DataBoundConstructor",
31880
- "DataBoundSetter"
31881
- ]);
31882
- var TIER_1_CLASS_ANNOTATIONS2 = /* @__PURE__ */ new Set([
31883
- // Spring MVC
31884
- "RestController",
31885
- "Controller",
31886
- // Spring stereotype beans (#136 — see header)
31887
- "Service",
31888
- "Repository",
31889
- "Component",
31890
- // JAX-RS resource class
31891
- "Path",
31892
- // Servlet 3.0 annotation-based servlet
31893
- "WebServlet",
31894
- // JSR-356 WebSocket endpoint
31895
- "ServerEndpoint",
31896
- // Declarative HTTP client (borderline — inclusive in ship 1; contract-defined
31897
- // parameter shapes are downstream-relevant trust boundaries)
31898
- "FeignClient"
31899
- ]);
31900
- var TIER_1_BY_SUPERTYPE = /* @__PURE__ */ new Map([
31901
- // Servlet API
31902
- ["HttpServlet", /* @__PURE__ */ new Set(["doGet", "doPost", "doPut", "doDelete", "doHead", "doOptions", "doTrace", "service"])],
31903
- ["GenericServlet", /* @__PURE__ */ new Set(["service"])],
31904
- ["Filter", /* @__PURE__ */ new Set(["doFilter"])],
31905
- // Spring web
31906
- ["HandlerInterceptor", /* @__PURE__ */ new Set(["preHandle", "postHandle", "afterCompletion"])],
31907
- ["AsyncHandlerInterceptor", /* @__PURE__ */ new Set(["preHandle", "postHandle", "afterCompletion", "afterConcurrentHandlingStarted"])],
31908
- // Spring boot CLI entry points
31909
- ["CommandLineRunner", /* @__PURE__ */ new Set(["run"])],
31910
- ["ApplicationRunner", /* @__PURE__ */ new Set(["run"])],
31911
- // Netty channel handlers (#154 — CVE-2022-26884 dolphinscheduler).
31912
- // `SimpleChannelInboundHandler<T>.channelRead0(ctx, msg)` is the
31913
- // standard typed read entry; `ChannelInboundHandler.channelRead(ctx,
31914
- // Object)` is the untyped base. `ChannelInboundHandlerAdapter` and
31915
- // `ChannelDuplexHandler` are the most-common adapter classes user
31916
- // handlers extend. `NettyRequestProcessor` is the dolphinscheduler-
31917
- // specific (and other Netty-RPC projects') wire-message processor
31918
- // surface — `process(channel, command)` is the network entry point.
31919
- ["SimpleChannelInboundHandler", /* @__PURE__ */ new Set(["channelRead0", "messageReceived"])],
31920
- ["ChannelInboundHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
31921
- ["ChannelInboundHandlerAdapter", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
31922
- ["ChannelDuplexHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
31923
- ["NettyRequestProcessor", /* @__PURE__ */ new Set(["process"])],
31924
- // XStream deserialization converters (#224 — CVE-2020-26217,
31925
- // CVE-2021-21345). The xstream deserializer invokes `unmarshal`
31926
- // with attacker-controlled `HierarchicalStreamReader` state whenever
31927
- // untrusted XML reaches `XStream.fromXML`. Each converter is a
31928
- // deserialization gadget surface — its `unmarshal` parameters are
31929
- // network-facing taint sources even though the enclosing class
31930
- // carries no framework annotation. `marshal` is included for the
31931
- // symmetric round-trip surface but is rarely exploitable on its
31932
- // own; the fixup targets `unmarshal` primarily.
31933
- //
31934
- // `SingleValueConverter` is the string-form variant (fromString
31935
- // parses an attacker string; toString is the sink half of the
31936
- // round-trip). `ConverterMatcher` is the shared base and some
31937
- // downstream projects subclass it directly.
31938
- ["Converter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
31939
- ["SingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
31940
- ["ConverterMatcher", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
31941
- // XStream abstract base classes — direct-parent `extends` match
31942
- // covers the common shape where user converters subclass a base
31943
- // rather than implementing `Converter` directly.
31944
- ["AbstractReflectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal", "doMarshal", "doUnmarshal"])],
31945
- ["AbstractSingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
31946
- ["AbstractCollectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])]
31947
- ]);
31948
- var TIER_3_CLASS_SUFFIXES = [
31949
- "Util",
31950
- "Utils",
31951
- "Helper",
31952
- "Helpers"
31953
- ];
31954
- var TIER_3_PACKAGE_FRAGMENTS = [
31955
- ".template.",
31956
- ".templates.",
31957
- ".engine.",
31958
- ".engines."
31959
- ];
31960
- var TIER_3_JDK_FACADE_INTERFACES = /* @__PURE__ */ new Set([
31961
- // Collection root + common containers
31962
- "Collection",
31963
- "List",
31964
- "Set",
31965
- "Map",
31966
- "Queue",
31967
- "Deque",
31968
- "SortedSet",
31969
- "SortedMap",
31970
- "NavigableSet",
31971
- "NavigableMap",
31972
- // Iteration / ordering / equality contracts
31973
- "Iterator",
31974
- "Iterable",
31975
- "ListIterator",
31976
- "Comparator",
31977
- "Comparable",
31978
- // Serialization / cloning contracts
31979
- "Serializable",
31980
- "Externalizable",
31981
- "Cloneable"
31982
- ]);
31983
- function classNameLooksLikeUtility(name2) {
31984
- if (!name2) return false;
31985
- for (const suffix of TIER_3_CLASS_SUFFIXES) {
31986
- if (name2.length > suffix.length && name2.endsWith(suffix)) return true;
31987
- }
31988
- return false;
31989
- }
31990
- function packageLooksLikeTemplateOrEngine(pkg) {
31991
- if (!pkg) return false;
31992
- const padded = `.${pkg}.`;
31993
- for (const frag of TIER_3_PACKAGE_FRAGMENTS) {
31994
- if (padded.includes(frag)) return true;
31995
- }
31996
- return false;
31997
- }
31998
- function implementsJdkFacade(t) {
31999
- if (!t) return false;
32000
- for (const impl of t.implements ?? []) {
32001
- if (TIER_3_JDK_FACADE_INTERFACES.has(simpleTypeName(impl))) return true;
32002
- }
32003
- return false;
32004
- }
32005
- function classShapeIsLibraryFacade(enclosingType) {
32006
- if (!enclosingType) return false;
32007
- if (classNameLooksLikeUtility(enclosingType.name)) return true;
32008
- if (packageLooksLikeTemplateOrEngine(enclosingType.package)) return true;
32009
- if (implementsJdkFacade(enclosingType)) return true;
32010
- return false;
32011
- }
32012
- function annotationsInclude(annotations, targets) {
32013
- if (!annotations || annotations.length === 0) return false;
32014
- for (const raw of annotations) {
32015
- const simple = raw.replace(/^@/, "").replace(/[<(].*$/, "").trim();
32016
- if (targets.has(simple)) return true;
32017
- }
32018
- return false;
32019
- }
32020
- function simpleTypeName(ref) {
32021
- return ref.replace(/<.*$/, "").trim();
32022
- }
32023
- function looksLikeMainMethod(method) {
32024
- if (method.name !== "main") return false;
32025
- const params = method.parameters ?? [];
32026
- if (params.length !== 1) return false;
32027
- const t = (params[0].type ?? "").replace(/\s+/g, "");
32028
- return t === "String[]" || t === "String..." || t === "java.lang.String[]";
32029
- }
32030
- function methodIsSupertypeLifecycleEntryPoint(method, enclosingType) {
32031
- if (!method.name) return false;
32032
- if (!enclosingType) return false;
32033
- const candidates = [];
32034
- if (enclosingType.extends) candidates.push(simpleTypeName(enclosingType.extends));
32035
- for (const i2 of enclosingType.implements ?? []) candidates.push(simpleTypeName(i2));
32036
- for (const supertype of candidates) {
32037
- const lifecycleMethods = TIER_1_BY_SUPERTYPE.get(supertype);
32038
- if (lifecycleMethods?.has(method.name)) return true;
32039
- }
32040
- return false;
32041
- }
32042
- function classifyEntryPointTier(method, enclosingType, ctx) {
32043
- const language = (ctx.language ?? "").toLowerCase();
32044
- if (language !== "java") return "TIER_UNKNOWN";
32045
- if (!method) return "TIER_UNKNOWN";
32046
- if (classShapeIsLibraryFacade(enclosingType)) {
32047
- return "TIER_3_LIBRARY_API";
32048
- }
32049
- if (annotationsInclude(method.annotations, TIER_1_METHOD_ANNOTATIONS2)) {
32050
- return "TIER_1_ENTRY_POINT";
32051
- }
32052
- if (enclosingType && annotationsInclude(enclosingType.annotations, TIER_1_CLASS_ANNOTATIONS2)) {
32053
- return "TIER_1_ENTRY_POINT";
32054
- }
32055
- if (methodIsSupertypeLifecycleEntryPoint(method, enclosingType)) {
32056
- return "TIER_1_ENTRY_POINT";
32057
- }
32058
- if (looksLikeMainMethod(method)) {
32059
- return "TIER_1_ENTRY_POINT";
32060
- }
32061
- return "TIER_3_LIBRARY_API";
32062
- }
32063
- function shouldGateInterproceduralParam(sourceType, enclosingMethod, enclosingType, ctx) {
32064
- if (sourceType !== "interprocedural_param") return false;
32065
- if (!enclosingMethod) return false;
32066
- const tier = classifyEntryPointTier(enclosingMethod, enclosingType, ctx);
32067
- return tier === "TIER_3_LIBRARY_API";
32068
- }
32069
-
32070
32246
  // src/analysis/passes/interprocedural-pass.ts
32071
32247
  var InterproceduralPass = class {
32072
32248
  name = "interprocedural";
@@ -40743,10 +40919,14 @@ async function analyze(code, filePath, language, options = {}) {
40743
40919
  pipeline.add(new CliMainReflectionSuppressPass());
40744
40920
  if (!disabledPasses.has("library-profile-sink-gate"))
40745
40921
  pipeline.add(new LibraryProfileSinkGatePass());
40922
+ if (!disabledPasses.has("library-profile-xss-gate"))
40923
+ pipeline.add(new LibraryProfileXssGatePass());
40746
40924
  pipeline.add(new TaintPropagationPass());
40747
40925
  pipeline.add(new InterproceduralPass({
40748
40926
  enableEntryPointGate: options.enableEntryPointGate ?? true
40749
40927
  }));
40928
+ if (!disabledPasses.has("library-profile-cwe22-path-gate"))
40929
+ pipeline.add(new LibraryProfileCwe22PathGatePass());
40750
40930
  if (!disabledPasses.has("scan-secrets")) pipeline.add(new ScanSecretsPass());
40751
40931
  if (!disabledPasses.has("dead-code")) pipeline.add(new DeadCodePass());
40752
40932
  if (!disabledPasses.has("missing-await")) pipeline.add(new MissingAwaitPass());