Merge: flow-sensitive numeric var inference (completes assume_safe_numbers1)
ober
6ca4373ce0b5a37477487a35a0abc172a2be3ca9
--- 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) --- 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"))