Flow Go taint through full := assignment RHS
ober
1f55031e50607a2b7ed7c3706ce16a6bfbbc6d0b
--- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -31112,6 +31112,103 @@ (scan-positive-pattern-entry rule (cons 'pattern pattern) language path source target-root)))) (implicit-assignment-patterns language)))) + (def (go-simple-assignment-parts source first line-end) + (and (< first line-end) + (let ([c0 (string-ref source first)]) + (or (char-alphabetic? c0) (char=? c0 #\_))) + (let loop ([i (+ first 1)]) + (cond + [(and (< i line-end) + (identifier-token-char? (string-ref source i))) + (loop (+ i 1))] + [else + (let* ([l-end i] [j (skip-horizontal-forward source i)]) + (cond + [(and (< (+ j 1) line-end) + (char=? (string-ref source j) #\:) + (char=? (string-ref source (+ j 1)) #\=)) + (cons l-end (+ j 2))] + [(and (< j line-end) + (char=? (string-ref source j) #\=) + (or (>= (+ j 1) line-end) + (not (char=? + (string-ref source (+ j 1)) + #\=)))) + (cons l-end (+ j 1))] + [else #f]))])))) + (def (go-assignment-rhs-end source r-start line-end) + (let* ([raw-end (go-line-trimmed-end + source + r-start + line-end)] + [comment (string-find-substring-from source "//" r-start)] + [content-end (if (and comment (< comment raw-end)) + comment + raw-end)]) + (let loop ([e content-end]) + (if (and (> e r-start) + (let ([c (string-ref source (- e 1))]) + (or (char=? c #\space) (char=? c #\tab)))) + (loop (- e 1)) + e)))) + (def (scan-go-implicit-assignment-propagators + rule + path + source) + (let ([len (string-length source)]) + (let loop ([line-start 0] [acc '()]) + (if (> line-start len) + (reverse acc) + (let* ([line-end (line-end-after source line-start)] + [first (line-first-nonspace + source + line-start + line-end)] + [parts (go-simple-assignment-parts + source + first + line-end)] + [next (if (< line-end len) + (+ line-end 1) + (+ len 1))]) + (if (not parts) + (loop next acc) + (let* ([l-end (car parts)] + [op-end (cdr parts)] + [r-start (skip-horizontal-forward + source + op-end)] + [r-end (go-assignment-rhs-end + source + r-start + line-end)]) + (if (<= r-end r-start) + (loop next acc) + (let* ([l-binding (metavariable-binding-for-range + "L" + source + first + l-end)] + [r-binding (metavariable-binding-for-range + "R" + source + r-start + r-end)] + [finding (finding-for-range-with-bindings rule path source first r-end + (list + (cons "L" l-binding) + (cons "R" r-binding)))] + [propagator (list (cons 'finding finding) + (cons 'from "$R") + (cons 'to "$L") + (cons 'by-side-effect #t) + (cons 'label #f) + (cons 'requires #f) + (cons 'replace-labels #f) + (cons + 'implicit-assignment + #t))]) + (loop next (cons propagator acc))))))))))) (def (label-token-char? ch) (or (char-alphabetic? ch) (char-numeric? ch) @@ -32874,8 +32971,15 @@ [propagators (scan-taint-propagators rule (alist-ref/default taint 'propagators '()) language path source target-root)] - [implicit-propagators (scan-implicit-assignment-propagators rule language path source - target-root)] + [implicit-propagators (append + (scan-implicit-assignment-propagators rule language path source + target-root) + (if (go-language? language) + (scan-go-implicit-assignment-propagators + rule + path + source) + '()))] [sanitizers (scan-taint-specs rule (alist-ref/default taint 'sanitizers '()) language path source target-root #f)] [sources (expand-taint-sources rule initial-sources source-matches --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -3,11 +3,11 @@ ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") ("src/semgrep/parse/parse-target.ss" . "E74854DDDACF6BA") - ("src/semgrep/scan.ss" . "351112D8D439582D") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") + ("src/semgrep/scan.ss" . "3C8AA38427B2EE0A") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") ("src/semgrep/rule.ss" . "E12C108153C181FA") + ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") ("src/semgrep/match/structural.ss" . "6FE77014EE9FDCE4") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -31089,6 +31089,81 @@ target-root)))) (implicit-assignment-patterns language)))) +;; Go's `$L := $R` / `$L = $R` matching binds $R to only the head token of a +;; call RHS (e.g. `make` in `items := make(global.Items, ...)`), so taint does +;; not flow when the tainted value is nested inside the right-hand expression. +;; These helpers build implicit-assignment propagators whose `$R` spans the +;; whole right-hand side, so a tainted token anywhere in it propagates to `$L`. +(def (go-simple-assignment-parts source first line-end) + (and (< first line-end) + (let ([c0 (string-ref source first)]) + (or (char-alphabetic? c0) (char=? c0 #\_))) + (let loop ([i (+ first 1)]) + (cond + [(and (< i line-end) (identifier-token-char? (string-ref source i))) + (loop (+ i 1))] + [else + (let* ([l-end i] + [j (skip-horizontal-forward source i)]) + (cond + [(and (< (+ j 1) line-end) + (char=? (string-ref source j) #\:) + (char=? (string-ref source (+ j 1)) #\=)) + (cons l-end (+ j 2))] + [(and (< j line-end) + (char=? (string-ref source j) #\=) + (or (>= (+ j 1) line-end) + (not (char=? (string-ref source (+ j 1)) #\=)))) + (cons l-end (+ j 1))] + [else #f]))])))) + +(def (go-assignment-rhs-end source r-start line-end) + (let* ([raw-end (go-line-trimmed-end source r-start line-end)] + [comment (string-find-substring-from source "//" r-start)] + [content-end (if (and comment (< comment raw-end)) comment raw-end)]) + (let loop ([e content-end]) + (if (and (> e r-start) + (let ([c (string-ref source (- e 1))]) + (or (char=? c #\space) (char=? c #\tab)))) + (loop (- e 1)) + e)))) + +(def (scan-go-implicit-assignment-propagators rule path source) + (let ([len (string-length source)]) + (let loop ([line-start 0] [acc '()]) + (if (> line-start len) + (reverse acc) + (let* ([line-end (line-end-after source line-start)] + [first (line-first-nonspace source line-start line-end)] + [parts (go-simple-assignment-parts source first line-end)] + [next (if (< line-end len) (+ line-end 1) (+ len 1))]) + (if (not parts) + (loop next acc) + (let* ([l-end (car parts)] + [op-end (cdr parts)] + [r-start (skip-horizontal-forward source op-end)] + [r-end (go-assignment-rhs-end source r-start line-end)]) + (if (<= r-end r-start) + (loop next acc) + (let* ([l-binding (metavariable-binding-for-range + "L" source first l-end)] + [r-binding (metavariable-binding-for-range + "R" source r-start r-end)] + [finding (finding-for-range-with-bindings + rule path source first r-end + (list (cons "L" l-binding) + (cons "R" r-binding)))] + [propagator (list + (cons 'finding finding) + (cons 'from "$R") + (cons 'to "$L") + (cons 'by-side-effect #t) + (cons 'label #f) + (cons 'requires #f) + (cons 'replace-labels #f) + (cons 'implicit-assignment #t))]) + (loop next (cons propagator acc))))))))))) + (def (label-token-char? ch) (or (char-alphabetic? ch) (char-numeric? ch) @@ -32844,12 +32919,16 @@ source target-root)] [implicit-propagators - (scan-implicit-assignment-propagators - rule - language - path - source - target-root)] + (append + (scan-implicit-assignment-propagators + rule + language + path + source + target-root) + (if (go-language? language) + (scan-go-implicit-assignment-propagators rule path source) + '()))] [sanitizers (scan-taint-specs rule (alist-ref/default taint 'sanitizers '()) --- a/tests/smoke.ss +++ b/tests/smoke.ss @@ -4261,6 +4261,18 @@ (check (length findings) => 1) (check (finding-start-line (car findings)) => 5))) +(test-case "scan Go taint flows through := assignment and nested call" + (let* ([taint-config + "rules:\n - id: demo.taint.go.shortvar\n mode: taint\n languages: [go]\n message: go shortvar taint\n severity: WARNING\n pattern-sources:\n - pattern: getGlobal(...)\n pattern-sanitizers:\n - pattern: sanitizeGlobal(...)\n pattern-sinks:\n - pattern: sink(...)\n"] + [findings + (scan-config-string + taint-config + "go" + "demo.go" + "func f() {\n\tg := getGlobal()\n\titems := make(g.Items, len(g.items))\n\tsink(items)\n\tclean := sanitizeGlobal(g)\n\tsink(clean)\n}\n")]) + (check (length findings) => 1) + (check (finding-start-line (car findings)) => 4))) + (test-case "scan PHP taint ignores sink after break but resumes at case" (let* ([taint-config "rules:\n - id: demo.taint.php.break\n mode: taint\n languages: [php]\n message: unreachable php taint\n severity: WARNING\n pattern-sources:\n - pattern: $source\n pattern-sinks:\n - pattern: sink(...)\n"]