Merge: getter/setter field-sensitivity
ober
1e0d7546dc547a165308df42bfbc8f06127182db
--- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -31856,6 +31856,144 @@ (taint-state-finding source-state) source-text)]) (and arg-text (string-contains-token? arg-text st))))) + (def (enclosing-call-open source-text pos) + (let loop ([i (- pos 1)] [depth 0]) + (cond + [(< i 0) #f] + [(char=? (string-ref source-text i) #\)) + (loop (- i 1) (+ depth 1))] + [(char=? (string-ref source-text i) #\() + (if (= depth 0) i (loop (- i 1) (- depth 1)))] + [else (loop (- i 1) depth)]))) + (def (call-target-before source-text open-idx) + (let loop ([i (- open-idx 1)]) + (cond + [(< i 0) (string-trim (substring source-text 0 open-idx))] + [(let ([c (string-ref source-text i)]) + (or (identifier-char? c) (char=? c #\.))) + (loop (- i 1))] + [else + (string-trim (substring source-text (+ i 1) open-idx))]))) + (def (string-last-index s ch) + (let loop ([i (- (string-length s) 1)]) + (cond + [(< i 0) #f] + [(char=? (string-ref s i) ch) i] + [else (loop (- i 1))]))) + (def (capitalize-first s) + (if (= (string-length s) 0) + s + (string-append + (string (char-upcase (string-ref s 0))) + (substring s 1 (string-length s))))) + (def (lowercase-first s) + (if (= (string-length s) 0) + s + (string-append + (string (char-downcase (string-ref s 0))) + (substring s 1 (string-length s))))) + (def (setter-target-recv-prop target) + (let* ([dot (string-last-index target #\.)] + [recv (if dot (substring target 0 dot) "")] + [method (if dot + (substring + target + (+ dot 1) + (string-length target)) + target)]) + (and (sg-string-prefix? "set" method) + (> (string-length method) 3) + (char-upper-case? (string-ref method 3)) + (> (string-length recv) 0) + (cons recv (substring method 3 (string-length method)))))) + (def (field-assignment-recv-prop source source-text) + (let ([len (string-length source-text)]) + (let skip ([i (- (finding-start-offset source) 1)]) + (cond + [(< i 0) #f] + [(char-whitespace? (string-ref source-text i)) + (skip (- i 1))] + [(char=? (string-ref source-text i) #\=) + (and (or (= i 0) + (not (memv + (string-ref source-text (- i 1)) + '(#\= #\! #\< #\> #\+ #\- #\* #\/ #\% #\& #\| + #\^)))) + (let* ([end (let s2 ([j (- i 1)]) + (if (and (>= j 0) + (char-whitespace? + (string-ref source-text j))) + (s2 (- j 1)) + (+ j 1)))] + [start (let b ([k (- end 1)]) + (if (and (>= k 0) + (let ([c (string-ref + source-text + k)]) + (or (identifier-char? c) + (char=? c #\.)))) + (b (- k 1)) + (+ k 1)))] + [target (substring source-text start end)] + [dot (string-last-index target #\.)]) + (and dot + (> dot 0) + (< (+ dot 1) (string-length target)) + (cons + (substring target 0 dot) + (capitalize-first + (substring + target + (+ dot 1) + (string-length target)))))))] + [else #f])))) + (def (source-field-write-recv-prop source source-text) + (or (let ([open (enclosing-call-open + source-text + (finding-start-offset source))]) + (and open + (let ([target (call-target-before source-text open)]) + (and (> (string-length target) 0) + (let ([rp (setter-target-recv-prop target)]) + (and rp (list (car rp) (cdr rp) 'setter))))))) + (let ([rp (field-assignment-recv-prop source source-text)]) + (and rp (list (car rp) (cdr rp) 'field))))) + (def (text-has-field-access? text form) + (let ([flen (string-length form)] + [tlen (string-length text)]) + (let loop ([from 0]) + (let ([idx (string-find-substring-from text form from)]) + (and idx + (let ([after (+ idx flen)]) + (if (or (>= after tlen) + (not (identifier-char? + (string-ref text after)))) + #t + (loop (+ idx 1))))))))) + (def (source-reaches-via-getter-setter? + source-state + sink + source-text) + (let ([source (taint-state-finding source-state)]) + (and source + (let ([rpk (source-field-write-recv-prop + source + source-text)]) + (and rpk + (let* ([recv (car rpk)] + [prop (cadr rpk)] + [kind (caddr rpk)] + [sink-text (finding-text sink source-text)]) + (or (text-has-field-access? + sink-text + (string-append recv ".get" prop "()")) + (and (eq? kind 'setter) + (text-has-field-access? + sink-text + (string-append + recv + "." + (lowercase-first prop))))))))))) (def (line-indent line) (let ([len (string-length line)]) (let loop ([i 0]) @@ -31946,6 +32084,10 @@ source-state sink source-text) + (source-reaches-via-getter-setter? + source-state + sink + source-text) (and (taint-state-token? source-state) (source-token-in-sink/except? source @@ -33103,6 +33245,10 @@ source-text)) (source-base-of-sink-arg? source sink source-text) (source-token-in-sink-arg? source-state sink source-text) + (source-reaches-via-getter-setter? + source-state + sink + source-text) (and (finding-range-contains? sink source) (not (finding-range-equal? sink source)) (echo-or-print-statement-sink? sink source-text) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -31797,6 +31797,144 @@ [st (finding-text (taint-state-finding source-state) source-text)]) (and arg-text (string-contains-token? arg-text st))))) +;; --- getter/setter field-sensitivity --- +;; A value passed to `recv.setFoo(...)` is stored in field Foo and read back by +;; `recv.getFoo()`, so a source inside a setter call reaches a sink that reads +;; the paired getter (`sink(e.getX())` after `e.setX(tainted)`), while a +;; different field (`e.getY()`) does not. Pairing is by the capitalized field +;; name on the same receiver. +(def (enclosing-call-open source-text pos) + ;; index of the `(` whose argument list directly encloses pos, else #f. + (let loop ([i (- pos 1)] [depth 0]) + (cond + [(< i 0) #f] + [(char=? (string-ref source-text i) #\)) (loop (- i 1) (+ depth 1))] + [(char=? (string-ref source-text i) #\() + (if (= depth 0) i (loop (- i 1) (- depth 1)))] + [else (loop (- i 1) depth)]))) + +(def (call-target-before source-text open-idx) + ;; the dotted call target (`e.setX`, `setX`) immediately before `(`. + (let loop ([i (- open-idx 1)]) + (cond + [(< i 0) (string-trim (substring source-text 0 open-idx))] + [(let ([c (string-ref source-text i)]) + (or (identifier-char? c) (char=? c #\.))) + (loop (- i 1))] + [else (string-trim (substring source-text (+ i 1) open-idx))]))) + +(def (string-last-index s ch) + (let loop ([i (- (string-length s) 1)]) + (cond + [(< i 0) #f] + [(char=? (string-ref s i) ch) i] + [else (loop (- i 1))]))) + +(def (capitalize-first s) + (if (= (string-length s) 0) + s + (string-append (string (char-upcase (string-ref s 0))) + (substring s 1 (string-length s))))) + +(def (lowercase-first s) + (if (= (string-length s) 0) + s + (string-append (string (char-downcase (string-ref s 0))) + (substring s 1 (string-length s))))) + +(def (setter-target-recv-prop target) + ;; `e.setX` -> ("e" . "X") (capitalized property); #f if not a setter call. + (let* ([dot (string-last-index target #\.)] + [recv (if dot (substring target 0 dot) "")] + [method (if dot + (substring target (+ dot 1) (string-length target)) + target)]) + (and (sg-string-prefix? "set" method) + (> (string-length method) 3) + (char-upper-case? (string-ref method 3)) + (> (string-length recv) 0) + (cons recv (substring method 3 (string-length method)))))) + +(def (field-assignment-recv-prop source source-text) + ;; source is the RHS of `recv.field = ...` -> ("recv" . "Field") (capitalized). + (let ([len (string-length source-text)]) + (let skip ([i (- (finding-start-offset source) 1)]) + (cond + [(< i 0) #f] + [(char-whitespace? (string-ref source-text i)) (skip (- i 1))] + [(char=? (string-ref source-text i) #\=) + (and (or (= i 0) + (not (memv (string-ref source-text (- i 1)) + '(#\= #\! #\< #\> #\+ #\- #\* #\/ #\% #\& #\| #\^)))) + (let* ([end (let s2 ([j (- i 1)]) + (if (and (>= j 0) + (char-whitespace? (string-ref source-text j))) + (s2 (- j 1)) + (+ j 1)))] + [start (let b ([k (- end 1)]) + (if (and (>= k 0) + (let ([c (string-ref source-text k)]) + (or (identifier-char? c) (char=? c #\.)))) + (b (- k 1)) + (+ k 1)))] + [target (substring source-text start end)] + [dot (string-last-index target #\.)]) + (and dot (> dot 0) (< (+ dot 1) (string-length target)) + (cons (substring target 0 dot) + (capitalize-first + (substring target (+ dot 1) + (string-length target))))))) ] + [else #f])))) + +(def (source-field-write-recv-prop source source-text) + ;; (recv prop kind) the source is written into: kind 'setter from + ;; `recv.setX(source)`, kind 'field from `recv.x = source`. The kind matters + ;; for which read forms are NEW: a setter write has no plain-field propagator, + ;; so it must also reach the direct field `recv.x`; a direct field assignment + ;; is already handled (with clean-tracking) by the field propagator, so here it + ;; only needs the cross-form getter `recv.getX()`. + (or (let ([open (enclosing-call-open source-text + (finding-start-offset source))]) + (and open + (let ([target (call-target-before source-text open)]) + (and (> (string-length target) 0) + (let ([rp (setter-target-recv-prop target)]) + (and rp (list (car rp) (cdr rp) 'setter))))))) + (let ([rp (field-assignment-recv-prop source source-text)]) + (and rp (list (car rp) (cdr rp) 'field))))) + +(def (text-has-field-access? text form) + ;; `form` (e.g. `e.x`, `e.getX()`) occurs in text at an identifier boundary, + ;; so `e.x` matches `sink(e.x)` but not `sink(e.xyz)`. + (let ([flen (string-length form)] [tlen (string-length text)]) + (let loop ([from 0]) + (let ([idx (string-find-substring-from text form from)]) + (and idx + (let ([after (+ idx flen)]) + (if (or (>= after tlen) + (not (identifier-char? (string-ref text after)))) + #t + (loop (+ idx 1))))))))) + +(def (source-reaches-via-getter-setter? source-state sink source-text) + ;; a source written into field <prop> of <recv> reaches a sink that reads it + ;; back, via the getter `recv.get<Prop>()` or the direct field `recv.<prop>`. + (let ([source (taint-state-finding source-state)]) + (and source + (let ([rpk (source-field-write-recv-prop source source-text)]) + (and rpk + (let* ([recv (car rpk)] [prop (cadr rpk)] [kind (caddr rpk)] + [sink-text (finding-text sink source-text)]) + (or (text-has-field-access? + sink-text (string-append recv ".get" prop "()")) + ;; the direct field form is a NEW reach only for a setter + ;; write; a direct field assignment already propagates the + ;; plain field (with clean-tracking) on its own. + (and (eq? kind 'setter) + (text-has-field-access? + sink-text + (string-append recv "." (lowercase-first prop))))))))))) + (def (line-indent line) (let ([len (string-length line)]) (let loop ([i 0]) @@ -31882,6 +32020,8 @@ source-text) (source-base-of-sink-arg? source sink source-text) (source-token-in-sink-arg? source-state sink source-text) + (source-reaches-via-getter-setter? source-state sink + source-text) (and (taint-state-token? source-state) (source-token-in-sink/except? source @@ -32966,6 +33106,7 @@ (direct-call-argument-source? source sink source-text)) (source-base-of-sink-arg? source sink source-text) (source-token-in-sink-arg? source-state sink source-text) + (source-reaches-via-getter-setter? source-state sink source-text) (and (finding-range-contains? sink source) (not (finding-range-equal? sink source)) (echo-or-print-statement-sink? sink source-text)