Skip to content

circle-ir 3.38.0 — cross-file inter-procedural taint chains (#19)

Choose a tag to compare

@openmason openmason released this 12 Jun 06:21
· 38 commits to main since this release

Fixed

Cross-file inter-procedural taint chains now resolve through wrapper return values and sink-param summaries (#19).

Closes the Java Spring-shape gap reported for CVE-2011-2732 (sendRedirect open redirect via UrlHandler.determineTargetUrl wrapper) — and by virtue of the same fix, the Jenkins #1 shape (@DataBoundConstructor field bound to user input flowing through BuildStepCommandRunner.runRuntime.exec) and CVE-2018-1260 (Spring SpEL injection through SpelHelper.parseExpression + getValue wrapper).

After diagnostic review the issue was reframed: it is not Spring-specific. The engine already had every intermediate signal — sources per file, sinks per file, the intra-file interprocedural_param → sink flow in the sink wrapper, and cross-file call resolution with args_mapping. Only the chaining between them was missing.

Root cause — three independent gaps in + "CrossFileResolver" +

  1. + "isMethodTaintSource" + treated + "interprocedural_param" + sources as "real", so every internal helper with typed parameters was marked + "returnsSource = true" + . Cross-file + "wrapper(...)" + calls would then ghost-taint their callers.
  2. + "findTaintedParams" + only looked at annotations ( + "@RequestParam" + / + "@RequestBody" + / + "@PathVariable" + ) — so a sink-wrapper like + "RedirectStrategy.sendRedirect(req, res, String url) { res.sendRedirect(url); }" + carried + "taintedParams = []" + , and the + "args_mapping[].taint_propagates" + summary on every cross-file call was permanently stuck at + "false" + .
  3. No chaining method existed. + "findCrossFileTaintFlows()" + only emits + "source-in-caller → sink-in-callee" + flows; it cannot see the canonical 2-wrapper chain + "source-in-callee-A → wrapper-return-in-caller → sink-call-in-caller → sink-in-callee-B" + , even though + "callee-A.returnsSource=true" + + + "callee-B.taintedParams=[2]" + is the exact summary needed to link them.
  4. + "findCrossFileTaintFlows()" + overapproximated when the caller had its own real source: it emitted a path to any downstream cross-file sink regardless of whether the call's arguments actually carried the source.

Fix — four minimal changes

  1. + "isMethodTaintSource" + + + "getSourceType" + skip + "interprocedural_param" + sources entirely.
  2. + "findTaintedParams" + adds a sink-arg-matching heuristic: for every known sink inside the method body, scan the call expression's argument variables and whole-word-match them against the method's parameter names. Hits → + "taintedParams" + .
  3. New + "findInterproceduralTaintPaths()" + walks each caller method, seeds tainted map from real sources, marks DFG + "local" + defs tainted on + "returnsSource" + callee, emits multi-hop + "TaintPath" + when a tainted arg reaches a callee's + "taintedParam" + . Confidence: 0.85 per hop, floor 0.30.
  4. Variable-connectivity gate on + "findCrossFileTaintFlows()" + : require the cross-file call's args to mention the source's owning variable (eliminates sanitized-wrapper FPs).

CrossFilePass

  • Emits new section "1b. Inter-procedural multi-hop taint chains" with dedup against direct cross-file flows.
  • Populates + "cross_file_calls[].args_mapping[].taint_propagates" + from the callee's analyzed + "taintedParams" + summary (was hard-coded + "false" + ).

Verification

  • 4 new fixtures in + "tests/analysis/project-graph.test.ts" + :
    • CVE-2011-2732 + "sendRedirect" + open-redirect shape (positive)
    • Sanitized-wrapper negative control (must not emit)
    • CVE-2018-1260 SpEL + "parseExpression" + + + "getValue" + shape
    • Jenkins #1 + "@DataBoundConstructor" + + "Runtime.exec" + shape
  • All 1935 tests pass (1931 baseline + 4 new). Typecheck clean.
  • OWASP Benchmark Java + Juliet (156/156) + SecuriBench Micro: no regression.

Why this is not a redesign

Every primitive used by the new chaining logic — + "resolveCall" + , + "methodTaintInfo" + , + "ir.dfg.defs" + , + "ir.taint.sources/sinks" + — already existed. No new IR types, no new pipeline pass, no new config files. The fix is purely in how CrossFileResolver consumes existing per-file analysis results.

Full diff: circle-ir-v3.37.0...circle-ir-v3.38.0