Structural taint engine: flow-sensitive numeric var inference (assume-safe step 3c)

ober

16785e8a9e3c79bcf99f4971dbdb9673288bda0f

diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index d3cfe39..1d35f61 100644
--- a/lib/semgrep/scan.sls
+++ b/lib/semgrep/scan.sls
@@ -32042,12 +32042,130 @@
              (string-find-substring t "Byte.parseByte(")
              (string-find-substring t "Double.parseDouble(")
              (string-find-substring t "Float.parseFloat("))))
+  (def (java-numeric-literal? t)
+       (let* ([s (string-trim t)] [len (string-length s)])
+         (and (> len 0)
+              (let ([start (if (memv (string-ref s 0) '(#\- #\+)) 1 0)])
+                (and (< start len)
+                     (char-numeric? (string-ref s start))
+                     (let loop ([i start])
+                       (or (>= i len)
+                           (let ([c (string-ref s i)])
+                             (and (or (char-numeric? c)
+                                      (memv
+                                        c
+                                        '(#\. #\_ #\x #\X #\e #\E #\a #\b
+                                              #\c #\d #\f #\A #\B #\C #\D
+                                              #\F #\l #\L)))
+                                  (loop (+ i 1)))))))))))
+  (def (java-split-top-level-plus t)
+       (let ([len (string-length t)])
+         (let loop ([i 0] [depth 0] [in-str #f] [start 0] [acc '()])
+           (if (>= i len)
+               (reverse (cons (substring t start len) acc))
+               (let ([c (string-ref t i)])
+                 (cond
+                   [in-str
+                    (loop (+ i 1) depth (not (char=? c #\")) start acc)]
+                   [(char=? c #\") (loop (+ i 1) depth #t start acc)]
+                   [(or (char=? c #\() (char=? c #\[))
+                    (loop (+ i 1) (+ depth 1) in-str start acc)]
+                   [(or (char=? c #\)) (char=? c #\]))
+                    (loop (+ i 1) (- depth 1) in-str start acc)]
+                   [(and (char=? c #\+) (= depth 0))
+                    (loop (+ i 1) depth in-str (+ i 1)
+                      (cons (substring t start i) acc))]
+                   [else (loop (+ i 1) depth in-str start acc)]))))))
+  (def (java-has-top-level-plus? t)
+       (pair? (cdr (java-split-top-level-plus t))))
+  (def (java-assignment-eq-after? source-text pos)
+       (let ([len (string-length source-text)])
+         (let loop ([i pos])
+           (cond
+             [(>= i len) #f]
+             [(memv (string-ref source-text i) '(#\space #\tab))
+              (loop (+ i 1))]
+             [(char=? (string-ref source-text i) #\=)
+              (and (or (= (+ i 1) len)
+                       (not (char=? (string-ref source-text (+ i 1)) #\=)))
+                   i)]
+             [else #f]))))
+  (def (java-nearest-assignment-rhs
+         source-text
+         var
+         before-offset)
+       (let ([vlen (string-length var)]
+             [slen (string-length source-text)])
+         (let scan ([from 0] [best #f])
+           (let ([idx (string-find-substring-from
+                        source-text
+                        var
+                        from)])
+             (if (or (not idx) (> (+ idx vlen) before-offset))
+                 best
+                 (let ([before-ch (if (> idx 0)
+                                      (string-ref source-text (- idx 1))
+                                      #\space)]
+                       [eq (java-assignment-eq-after?
+                             source-text
+                             (+ idx vlen))])
+                   (if (and eq
+                            (not (identifier-char? before-ch))
+                            (not (char=? before-ch #\.)))
+                       (let* ([rhs-start (+ eq 1)]
+                              [semi (or (string-find-substring-from
+                                          source-text
+                                          ";"
+                                          rhs-start)
+                                        slen)]
+                              [rhs (string-trim
+                                     (substring
+                                       source-text
+                                       rhs-start
+                                       semi))])
+                         (scan (+ idx vlen) rhs))
+                       (scan (+ idx vlen) best))))))))
+  (def (java-value-numeric?
+         expr
+         source-text
+         before-offset
+         depth)
+       (and (< depth 8)
+            (let ([t (string-trim expr)])
+              (cond
+                [(java-has-top-level-plus? t)
+                 (all?
+                   (lambda (op)
+                     (java-value-numeric?
+                       op
+                       source-text
+                       before-offset
+                       (+ depth 1)))
+                   (java-split-top-level-plus t))]
+                [(java-numeric-value-expr? t) #t]
+                [(java-numeric-literal? t) #t]
+                [(simple-identifier-text? t)
+                 (let ([rhs (java-nearest-assignment-rhs
+                              source-text
+                              t
+                              before-offset)])
+                   (and rhs
+                        (not (string=? (string-trim rhs) t))
+                        (java-value-numeric?
+                          rhs
+                          source-text
+                          before-offset
+                          (+ depth 1))))]
+                [else #f]))))
   (def (sink-value-numeric? sink-spec source-text)
        (let* ([sink (sink-finding sink-spec)]
               [val (and sink
                         (or (direct-call-argument-text sink source-text)
-                            (finding-text sink source-text)))])
-         (and val (java-numeric-value-expr? val))))
+                            (finding-text sink source-text)))]
+              [before (and sink (finding-start-offset sink))])
+         (and val
+              before
+              (java-value-numeric? val source-text before 0))))
   (def (taint-assume-safe-indexes? rule)
        (rule-option-enabled? rule "taint_assume_safe_indexes"))
   (def (taint-assume-safe-functions? rule)
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index 77fa992..afa26b8 100644
--- a/src/semgrep/scan.ss
+++ b/src/semgrep/scan.ss
@@ -31974,12 +31974,117 @@
         (string-find-substring t "Double.parseDouble(")
         (string-find-substring t "Float.parseFloat("))))
 
+;; --- flow-sensitive numeric/string inference over local assignments ---
+;; Completes assume_safe_numbers: a sink value that is a local variable is safe
+;; iff the variable's assignment chain is numeric (test6 `w = v + x.length`,
+;; all-int) and unsafe iff string (test7 `w = v + "a"...`, a String concat).
+(def (java-numeric-literal? t)
+  (let* ([s (string-trim t)] [len (string-length s)])
+    (and (> len 0)
+         (let ([start (if (memv (string-ref s 0) '(#\- #\+)) 1 0)])
+           (and (< start len)
+                (char-numeric? (string-ref s start))
+                (let loop ([i start])
+                  (or (>= i len)
+                      (let ([c (string-ref s i)])
+                        (and (or (char-numeric? c)
+                                 (memv c '(#\. #\_ #\x #\X #\e #\E #\a #\b
+                                           #\c #\d #\f #\A #\B #\C #\D #\F
+                                           #\l #\L)))
+                             (loop (+ i 1)))))))))))
+
+(def (java-split-top-level-plus t)
+  ;; split on `+` at paren/bracket depth 0, outside string literals.
+  (let ([len (string-length t)])
+    (let loop ([i 0] [depth 0] [in-str #f] [start 0] [acc '()])
+      (if (>= i len)
+          (reverse (cons (substring t start len) acc))
+          (let ([c (string-ref t i)])
+            (cond
+              [in-str (loop (+ i 1) depth (not (char=? c #\")) start acc)]
+              [(char=? c #\") (loop (+ i 1) depth #t start acc)]
+              [(or (char=? c #\() (char=? c #\[))
+               (loop (+ i 1) (+ depth 1) in-str start acc)]
+              [(or (char=? c #\)) (char=? c #\]))
+               (loop (+ i 1) (- depth 1) in-str start acc)]
+              [(and (char=? c #\+) (= depth 0))
+               (loop (+ i 1) depth in-str (+ i 1)
+                     (cons (substring t start i) acc))]
+              [else (loop (+ i 1) depth in-str start acc)]))))))
+
+(def (java-has-top-level-plus? t)
+  (pair? (cdr (java-split-top-level-plus t))))
+
+(def (java-assignment-eq-after? source-text pos)
+  ;; from pos skip horizontal ws; return index of a single `=` (not `==`), else #f.
+  (let ([len (string-length source-text)])
+    (let loop ([i pos])
+      (cond
+        [(>= i len) #f]
+        [(memv (string-ref source-text i) '(#\space #\tab)) (loop (+ i 1))]
+        [(char=? (string-ref source-text i) #\=)
+         (and (or (= (+ i 1) len)
+                  (not (char=? (string-ref source-text (+ i 1)) #\=)))
+              i)]
+        [else #f]))))
+
+(def (java-nearest-assignment-rhs source-text var before-offset)
+  ;; the nearest preceding `... var = RHS ;` before before-offset; RHS text or #f.
+  (let ([vlen (string-length var)] [slen (string-length source-text)])
+    (let scan ([from 0] [best #f])
+      (let ([idx (string-find-substring-from source-text var from)])
+        (if (or (not idx) (> (+ idx vlen) before-offset))
+            best
+            (let ([before-ch (if (> idx 0)
+                                 (string-ref source-text (- idx 1))
+                                 #\space)]
+                  [eq (java-assignment-eq-after? source-text (+ idx vlen))])
+              (if (and eq
+                       (not (identifier-char? before-ch))
+                       (not (char=? before-ch #\.)))
+                  (let* ([rhs-start (+ eq 1)]
+                         [semi (or (string-find-substring-from
+                                     source-text ";" rhs-start)
+                                   slen)]
+                         [rhs (string-trim (substring source-text rhs-start semi))])
+                    (scan (+ idx vlen) rhs))
+                  (scan (+ idx vlen) best))))))))
+
+(def (java-value-numeric? expr source-text before-offset depth)
+  ;; expr's type is provably numeric: a numeric literal, an atomic numeric
+  ;; expression (`.length`/cast/`compareTo`/`parseInt`, even with String args),
+  ;; an all-numeric `+` (so a `+` with a String operand is NOT numeric), or a
+  ;; local var whose assignment chain is numeric. Anything not provably numeric
+  ;; (a String, an unknown object) returns #f, so the sink is left to flag —
+  ;; the safe default under assume_safe_numbers. Checking top-level `+` before
+  ;; the atomic numeric test keeps `"a" + x.length` (String concat) unsafe while
+  ;; `x.compareTo("safe")` (a numeric call with a String arg) stays numeric.
+  (and (< depth 8)
+       (let ([t (string-trim expr)])
+         (cond
+           [(java-has-top-level-plus? t)
+            (all? (lambda (op)
+                    (java-value-numeric? op source-text before-offset
+                                         (+ depth 1)))
+                  (java-split-top-level-plus t))]
+           [(java-numeric-value-expr? t) #t]
+           [(java-numeric-literal? t) #t]
+           [(simple-identifier-text? t)
+            (let ([rhs (java-nearest-assignment-rhs source-text t before-offset)])
+              (and rhs
+                   (not (string=? (string-trim rhs) t))
+                   (java-value-numeric? rhs source-text before-offset
+                                        (+ depth 1))))]
+           [else #f]))))
+
 (def (sink-value-numeric? sink-spec source-text)
   (let* ([sink (sink-finding sink-spec)]
          [val (and sink
                    (or (direct-call-argument-text sink source-text)
-                       (finding-text sink source-text)))])
-    (and val (java-numeric-value-expr? val))))
+                       (finding-text sink source-text)))]
+         [before (and sink (finding-start-offset sink))])
+    (and val before
+         (java-value-numeric? val source-text before 0))))
 
 (def (taint-assume-safe-indexes? rule)
   (rule-option-enabled? rule "taint_assume_safe_indexes"))