Merge: getter/setter field-sensitivity

ober

1e0d7546dc547a165308df42bfbc8f06127182db

diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index e900db9..ba65723 100644
--- 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)
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index f1884da..142338f 100644
--- 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)