Merge: receiver-taint base-flow (method calls / field chains)
ober
591bfbeb8ab4f66547be55fe056d501da63858f0
--- a/HANDOFF_OPUS_4_8.md +++ b/HANDOFF_OPUS_4_8.md @@ -201,13 +201,32 @@ languages, so closing them is the tip of the larger work: Fix: a finding whose own line is a function-scope line takes that line as its scope, so a parameter is scoped to its own method. Non-regressing (all structural langs unchanged, smoke 321/321). This unblocks the param-source chain. - STILL TODO for assume-safe: (2) numeric/boolean source+value type-classification - (helpers were prototyped: `source-state-numeric-typed-param?` classifies - `int`/`long`/`Integer`… correctly via `param-type-before-name`), THEN (3) - numeric-result value-analysis (`Integer.valueOf`, `+`, `!=`). The `test` rule-id - group needs (2)+(3) + best-fit-sink, getter/setter, lambda, jpa, FQN before its - overfit clause retires. The same engine retires the other languages' taint - handlers + unblocks dart. + **(2) Numeric-typed param source classification — BUILT + merged (903f84f).** + `source-state-numeric-typed-param?` (via `param-type-before-name`) drops + `int`/`long`/`Integer`… param sources under assume_safe_numbers. With (1) it + cut assume_safe_numbers1 FPs 8→2 (handler off). + **(3a) Base-flow: receiver-taint — BUILT (2026-06-02).** Debugging + assume_safe_numbers1 with the option removed revealed the base param-source + flow itself dropped any value derived through a method call: `access-path-from-text` + returned #f at the first `(`, so `x.foo()`/`x.getSomething()` had no path and + never reached, while bare field `x.length` did. Fix is receiver-anchored via + `field-source-taints-base-text?` (EXPR is BASE + `.`/`[`): added as a reach + condition in three places — `source-base-of-sink-arg?` for `sink(x.foo())`, + `source-compatible-with-sink/except?` (the binding gate), and + `source-taints-binding?` for propagator assignments `t = x.foo(); sink(t)`. + Receiver-anchored so it never matches an arg inside another call + (`other(x)` / the assume_safe_functions wrapper test stays blocked). Validated: + smoke 321/321, taint-subset upstream-sweep 162/162 (0 mismatch, 0 error). + NOTE: did NOT touch `access-path-from-text` globally — that broke 3 smoke + taint tests by making `direct-call-argument-text` dig into inner-call args. + **(3b) STILL TODO base-flow:** argument-taint `sink(f(x))` (x is an ARG not a + receiver — test8/9/10 `Integer.valueOf(x)`), and binary-expr / cast RHS in + propagators (`w = v + x.getSomething()` test7, `t = (int)x.getSomething()` + test5) which `field-source-taints-base-text?` won't anchor. THEN (3c) + value-analysis (`.length`→numeric test4, string-concat test7). + The `test` rule-id group needs (2)+(3a/b/c) + best-fit-sink, getter/setter, + lambda, jpa, FQN before its overfit clause retires. The same engine retires the + other languages' taint handlers + unblocks dart. - a few per-language pattern hard-cases (go constant-folding/struct_tags, php metavar-call/non-prim-type, rust macro-call, kotlin named-ellipsis). --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -31812,6 +31812,20 @@ (metavariable-binding-text (cdr entry)) arg-text)) (finding-metavars source)))))) + (def (source-base-of-sink-arg? source sink source-text) + (let ([arg-text (direct-call-argument-text + sink + source-text)]) + (and arg-text + (or (field-source-taints-base-text? + arg-text + (finding-text source source-text)) + (any? + (lambda (entry) + (field-source-taints-base-text? + arg-text + (metavariable-binding-text (cdr entry)))) + (finding-metavars source)))))) (def (line-indent line) (let ([len (string-length line)]) (let loop ([i 0]) @@ -31894,6 +31908,10 @@ source sink source-text) + (source-base-of-sink-arg? + source + sink + source-text) (and (taint-state-token? source-state) (source-token-in-sink/except? source @@ -32878,6 +32896,7 @@ source sink source-text)) + (source-base-of-sink-arg? source sink source-text) (and (finding-range-contains? sink source) (not (finding-range-equal? sink source)) (echo-or-print-statement-sink? sink source-text) @@ -33112,7 +33131,10 @@ (finding-range-contains? source-finding from-finding) (string=? (finding-text source-finding source) - (metavariable-binding-text from-binding))))))) + (metavariable-binding-text from-binding)) + (field-source-taints-base-text? + (metavariable-binding-text from-binding) + (finding-text source-finding source))))))) (def (propagator-blocked-by-sanitizer? source-state from-finding --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -31757,6 +31757,24 @@ arg-text)) (finding-metavars source)))))) +(def (source-base-of-sink-arg? source sink source-text) + ;; The sink's argument is a member/index/method access whose receiver is the + ;; source variable (`sink(x.foo())`, `sink(x.length)`): a value derived from + ;; the tainted receiver reaches the sink. Mirrors how a bare `sink(x)` reaches + ;; via the access path, but covers the call-paren case the path parser stops + ;; at. Receiver-anchored, so it never matches a source that is merely an + ;; argument of some other call (`other(x)`). + (let ([arg-text (direct-call-argument-text sink source-text)]) + (and arg-text + (or (field-source-taints-base-text? + arg-text + (finding-text source source-text)) + (any? (lambda (entry) + (field-source-taints-base-text? + arg-text + (metavariable-binding-text (cdr entry)))) + (finding-metavars source)))))) + (def (line-indent line) (let ([len (string-length line)]) (let loop ([i 0]) @@ -31840,6 +31858,7 @@ source sink source-text) + (source-base-of-sink-arg? source sink source-text) (and (taint-state-token? source-state) (source-token-in-sink/except? source @@ -32750,6 +32769,7 @@ (finding-metavars-all-self? source)))) (and (not (taint-state-token? source-state)) (direct-call-argument-source? source sink source-text)) + (source-base-of-sink-arg? source sink source-text) (and (finding-range-contains? sink source) (not (finding-range-equal? sink source)) (echo-or-print-statement-sink? sink source-text) @@ -33016,7 +33036,15 @@ (finding-range-contains? source-finding from-finding) (string=? (finding-text source-finding source) - (metavariable-binding-text from-binding))))))) + (metavariable-binding-text from-binding)) + ;; `from` is a method call / field access on the source + ;; variable (`x.foo()`, `x.length`): invoking/indexing a + ;; tainted receiver yields a tainted value, so the assigned + ;; target carries the taint. The same-scope + before-or-at + ;; gates above keep this name-based reach within one method. + (field-source-taints-base-text? + (metavariable-binding-text from-binding) + (finding-text source-finding source))))))) (def (propagator-blocked-by-sanitizer? source-state from-finding sanitizers source) (any? (lambda (sanitizer-state)