Close go/command-injection via structural matcher (taint 9/9)
ober
08a9470cf04941052b0014861f25f503e2c8d3e8
--- a/lib/semgrep/match/structural.sls +++ b/lib/semgrep/match/structural.sls @@ -46,6 +46,58 @@ (string=? language "ts") (string=? language "tsx"))) (def (go-language? language) (string=? language "go")) + (def (go-skip-spaces source i len) + (if (and (< i len) (char-whitespace? (string-ref source i))) + (go-skip-spaces source (+ i 1) len) + i)) + (def (go-scan-ident source i len) + (if (and (< i len) (identifier-rest? (string-ref source i))) + (go-scan-ident source (+ i 1) len) + i)) + (def (go-find-matching-paren source open len) + (let loop ([i (+ open 1)] [depth 1]) + (cond + [(>= i len) #f] + [(char=? (string-ref source i) #\() + (loop (+ i 1) (+ depth 1))] + [(char=? (string-ref source i) #\)) + (if (= depth 1) i (loop (+ i 1) (- depth 1)))] + [else (loop (+ i 1) depth)]))) + (def (go-strip-typed-metavars source) + (let ([len (string-length source)]) + (let loop ([i 0] [acc '()]) + (if (>= i len) + (list->string (reverse acc)) + (let ([ch (string-ref source i)]) + (if (char=? ch #\() + (let ([j (go-skip-spaces source (+ i 1) len)]) + (if (and (< j len) + (char=? (string-ref source j) #\$) + (< (+ j 1) len) + (identifier-start? + (string-ref source (+ j 1)))) + (let* ([id-end (go-scan-ident + source + (+ j 1) + len)] + [k (go-skip-spaces source id-end len)] + [close (go-find-matching-paren + source + i + len)]) + (if (and (< k len) + (char=? (string-ref source k) #\:) + close) + (loop + (+ close 1) + (append + (reverse + (string->list + (substring source j id-end))) + acc)) + (loop (+ i 1) (cons ch acc)))) + (loop (+ i 1) (cons ch acc)))) + (loop (+ i 1) (cons ch acc)))))))) (def (sg-string-prefix? prefix s) (let ([prefix-len (string-length prefix)] [len (string-length s)]) @@ -160,8 +212,9 @@ ellipsis-token-name go-stmt-ellipsis-sentinel)) (def (rewrite-metavariables language source) - (let ([len (string-length source)] - [go? (go-language? language)]) + (let* ([go? (go-language? language)] + [source (if go? (go-strip-typed-metavars source) source)] + [len (string-length source)]) (let loop ([i 0] [state 'normal] [escaped? #f] @@ -1105,6 +1158,10 @@ target bindings) => (lambda (matches) matches)] + [(and (go-language? language) + (string=? (node-type target) "keyed_element") + (metavariable-name-from-text text)) + '()] [(quoted-ellipsis-metavariable-name-from-text text) => (lambda (name) (let ([next (if (string=? language "json") @@ -1431,8 +1488,35 @@ (begin (when child (node-close! child)) (child-search (+ i 1)))))))) + (def (go-keyed-element-shape? s) + (let ([len (string-length s)]) + (and (> len 0) + (let ([c0 (string-ref s 0)]) + (or (char-alphabetic? c0) (char=? c0 #\_))) + (let scan ([i 1]) + (cond + [(>= i len) #f] + [(identifier-rest? (string-ref s i)) (scan (+ i 1))] + [else + (let ([j (go-skip-spaces s i len)]) + (and (< j len) + (char=? (string-ref s j) #\:) + (or (>= (+ j 1) len) + (not (char=? + (string-ref s (+ j 1)) + #\=)))))]))))) + (def go-keyed-element-scaffold + (list + "var __sg_scaffold__ = __sg_KE{" + "}" + ellipsis-token-name)) (def (go-parse-pattern rewritten-pattern) - (let ([trimmed (string-trim rewritten-pattern)]) + (let* ([trimmed (string-trim rewritten-pattern)] + [go-pattern-scaffolds (if (go-keyed-element-shape? trimmed) + (cons + go-keyed-element-scaffold + go-pattern-scaffolds) + go-pattern-scaffolds)]) (let loop ([scaffolds go-pattern-scaffolds]) (if (null? scaffolds) (values #f #f) --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -31326,6 +31326,131 @@ (cons 'replace-labels #f) (cons 'implicit-assignment #t))]) (loop next (cons propagator acc))))))))) + (def (go-multi-blank-target? source start end) + (or (>= start end) + (and (= (- end start) 1) + (char=? (string-ref source start) #\_)))) + (def (go-split-lhs-targets source start end) + (let loop ([i start] + [seg-start (skip-horizontal-forward source start)] + [acc '()]) + (cond + [(>= i end) + (let ([tend (go-line-trimmed-end source seg-start end)]) + (reverse + (if (go-multi-blank-target? source seg-start tend) + acc + (cons (cons seg-start tend) acc))))] + [(char=? (string-ref source i) #\,) + (let ([tend (go-line-trimmed-end source seg-start i)]) + (loop + (+ i 1) + (skip-horizontal-forward source (+ i 1)) + (if (go-multi-blank-target? source seg-start tend) + acc + (cons (cons seg-start tend) acc))))] + [else (loop (+ i 1) seg-start acc)]))) + (def (go-multi-assignment-keyword? source first line-end) + (let ([end (let scan ([i first]) + (if (and (< i line-end) + (identifier-token-char? + (string-ref source i))) + (scan (+ i 1)) + i))]) + (and (member + (substring source first end) + '("for" "if" "switch" "select" "go" "defer" "return" "case" + "range")) + #t))) + (def (go-multi-assignment-parts source first line-end) + (and (< first line-end) + (not (go-multi-assignment-keyword? source first line-end)) + (let scan ([i first] [comma? #f]) + (cond + [(>= i line-end) #f] + [(let ([c (string-ref source i)]) + (or (identifier-token-char? c) (char=? c #\_))) + (scan (+ i 1) comma?)] + [(char=? (string-ref source i) #\,) (scan (+ i 1) #t)] + [(let ([c (string-ref source i)]) + (or (char=? c #\space) (char=? c #\tab))) + (scan (+ i 1) comma?)] + [(and (char=? (string-ref source i) #\:) + (< (+ i 1) line-end) + (char=? (string-ref source (+ i 1)) #\=)) + (and comma? + (go-multi-build source first i (+ i 2) line-end))] + [(and (char=? (string-ref source i) #\=) + (or (>= (+ i 1) line-end) + (not (char=? (string-ref source (+ i 1)) #\=)))) + (and comma? + (go-multi-build source first i (+ i 1) line-end))] + [else #f])))) + (def (go-multi-build source first lhs-end op-end line-end) + (let* ([targets (go-split-lhs-targets + source + first + (go-line-trimmed-end source first lhs-end))] + [r-start (skip-horizontal-forward source op-end)] + [r-end (go-assignment-rhs-end source r-start line-end)]) + (and (pair? targets) + (> r-end r-start) + (list targets r-start r-end)))) + (def (scan-go-multi-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-multi-assignment-parts + source + first + line-end)] + [next (if (< line-end len) + (+ line-end 1) + (+ len 1))]) + (if (not parts) + (loop next acc) + (let ([targets (car parts)] + [r-start (cadr parts)] + [r-end (caddr parts)]) + (loop + next + (append + (reverse + (map (lambda (tgt) + (let* ([l-binding (metavariable-binding-for-range + "L" + source + (car tgt) + (cdr tgt))] + [r-binding (metavariable-binding-for-range + "R" + source + r-start + r-end)] + [finding (finding-for-range-with-bindings rule path source + (car tgt) r-end + (list + (cons + "L" + l-binding) + (cons + "R" + r-binding)))]) + (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)))) + targets)) + acc))))))))) (def (scan-go-implicit-assignment-propagators rule path @@ -33205,6 +33330,10 @@ (scan-go-range-propagators rule path + source) + (scan-go-multi-assignment-propagators + rule + path source)) '()))] [sanitizers (scan-taint-specs rule (alist-ref/default taint 'sanitizers '()) --- 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" . "B1616180DE7038ED") - ("src/semgrep/scan.ss" . "A09A5F4FE16A2036") + ("src/semgrep/scan.ss" . "3E61EC4E4DEE7168") + ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") ("src/semgrep/rule.ss" . "E12C108153C181FA") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") - ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") - ("src/semgrep/match/structural.ss" . "633743099E28CC58") + ("src/semgrep/match/structural.ss" . "AA1BA746924C8189") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/match/structural.ss +++ b/src/semgrep/match/structural.ss @@ -57,6 +57,55 @@ (def (go-language? language) (string=? language "go")) +(def (go-skip-spaces source i len) + (if (and (< i len) (char-whitespace? (string-ref source i))) + (go-skip-spaces source (+ i 1) len) + i)) + +(def (go-scan-ident source i len) + (if (and (< i len) (identifier-rest? (string-ref source i))) + (go-scan-ident source (+ i 1) len) + i)) + +(def (go-find-matching-paren source open len) + (let loop ([i (+ open 1)] [depth 1]) + (cond + [(>= i len) #f] + [(char=? (string-ref source i) #\() (loop (+ i 1) (+ depth 1))] + [(char=? (string-ref source i) #\)) + (if (= depth 1) i (loop (+ i 1) (- depth 1)))] + [else (loop (+ i 1) depth)]))) + +;; Inline typed metavariable `($X : TYPE)` -> `$X`. Go's parser rejects the +;; `: TYPE` annotation, and without a type environment the type cannot be +;; checked, so we approximate by stripping it (the accompanying +;; metavariable-regex / structure usually constrains the match enough). +(def (go-strip-typed-metavars source) + (let ([len (string-length source)]) + (let loop ([i 0] [acc '()]) + (if (>= i len) + (list->string (reverse acc)) + (let ([ch (string-ref source i)]) + (if (char=? ch #\() + (let ([j (go-skip-spaces source (+ i 1) len)]) + (if (and (< j len) + (char=? (string-ref source j) #\$) + (< (+ j 1) len) + (identifier-start? (string-ref source (+ j 1)))) + (let* ([id-end (go-scan-ident source (+ j 1) len)] + [k (go-skip-spaces source id-end len)] + [close (go-find-matching-paren source i len)]) + (if (and (< k len) + (char=? (string-ref source k) #\:) + close) + (loop (+ close 1) + (append (reverse (string->list + (substring source j id-end))) + acc)) + (loop (+ i 1) (cons ch acc)))) + (loop (+ i 1) (cons ch acc)))) + (loop (+ i 1) (cons ch acc)))))))) + (def (sg-string-prefix? prefix s) (let ([prefix-len (string-length prefix)] [len (string-length s)]) @@ -189,8 +238,9 @@ go-stmt-ellipsis-sentinel)) (def (rewrite-metavariables language source) - (let ([len (string-length source)] - [go? (go-language? language)]) + (let* ([go? (go-language? language)] + [source (if go? (go-strip-typed-metavars source) source)] + [len (string-length source)]) (let loop ([i 0] [state 'normal] [escaped? #f] [paren-depth 0] [brace-depth 0] [acc '()]) (if (= i len) @@ -1104,6 +1154,12 @@ => (lambda (matches) matches)] [(go-import-declaration-matches language pattern target bindings) => (lambda (matches) matches)] + ;; A positional metavariable element does not match a keyed composite + ;; literal element: `&T{$X, ...}` must not match `&T{Key: v, ...}`. + [(and (go-language? language) + (string=? (node-type target) "keyed_element") + (metavariable-name-from-text text)) + '()] [(quoted-ellipsis-metavariable-name-from-text text) => (lambda (name) (let ([next (if (string=? language "json") @@ -1417,10 +1473,35 @@ (when child (node-close! child)) (child-search (+ i 1)))))))) +;; A pattern shaped like `IDENT : ...` (not `:=`) is a composite-literal keyed +;; element (e.g. `Path: $PATH`). Under the func-body scaffold it would parse as +;; a labeled statement, so we try a composite-literal scaffold first. +(def (go-keyed-element-shape? s) + (let ([len (string-length s)]) + (and (> len 0) + (let ([c0 (string-ref s 0)]) + (or (char-alphabetic? c0) (char=? c0 #\_))) + (let scan ([i 1]) + (cond + [(>= i len) #f] + [(identifier-rest? (string-ref s i)) (scan (+ i 1))] + [else + (let ([j (go-skip-spaces s i len)]) + (and (< j len) + (char=? (string-ref s j) #\:) + (or (>= (+ j 1) len) + (not (char=? (string-ref s (+ j 1)) #\=)))))]))))) + +(def go-keyed-element-scaffold + (list "var __sg_scaffold__ = __sg_KE{" "}" ellipsis-token-name)) + (def (go-parse-pattern rewritten-pattern) ;; Returns (values pattern-node parse-result) on success, or (values #f #f). ;; Caller must node-close! the node and tree-close! the parse-result tree. - (let ([trimmed (string-trim rewritten-pattern)]) + (let* ([trimmed (string-trim rewritten-pattern)] + [go-pattern-scaffolds (if (go-keyed-element-shape? trimmed) + (cons go-keyed-element-scaffold go-pattern-scaffolds) + go-pattern-scaffolds)]) (let loop ([scaffolds go-pattern-scaffolds]) (if (null? scaffolds) (values #f #f) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -31283,6 +31283,108 @@ (cons 'implicit-assignment #t))]) (loop next (cons propagator acc))))))))) +(def (go-multi-blank-target? source start end) + (or (>= start end) + (and (= (- end start) 1) (char=? (string-ref source start) #\_)))) + +(def (go-split-lhs-targets source start end) + ;; Split [start,end) on commas; return (tstart . tend) for each non-`_` + ;; trimmed segment. + (let loop ([i start] [seg-start (skip-horizontal-forward source start)] [acc '()]) + (cond + [(>= i end) + (let ([tend (go-line-trimmed-end source seg-start end)]) + (reverse (if (go-multi-blank-target? source seg-start tend) + acc + (cons (cons seg-start tend) acc))))] + [(char=? (string-ref source i) #\,) + (let ([tend (go-line-trimmed-end source seg-start i)]) + (loop (+ i 1) + (skip-horizontal-forward source (+ i 1)) + (if (go-multi-blank-target? source seg-start tend) + acc + (cons (cons seg-start tend) acc))))] + [else (loop (+ i 1) seg-start acc)]))) + +(def (go-multi-assignment-keyword? source first line-end) + (let ([end (let scan ([i first]) + (if (and (< i line-end) (identifier-token-char? (string-ref source i))) + (scan (+ i 1)) + i))]) + (and (member (substring source first end) + '("for" "if" "switch" "select" "go" "defer" "return" "case" "range")) + #t))) + +(def (go-multi-assignment-parts source first line-end) + ;; `T1, T2, ... := RHS` (or `=`) with at least one comma in the LHS and only + ;; identifiers/commas/`_` before the operator. Returns (targets r-start r-end) + ;; or #f. Excludes lines led by a control keyword (for/if/...). + (and (< first line-end) + (not (go-multi-assignment-keyword? source first line-end)) + (let scan ([i first] [comma? #f]) + (cond + [(>= i line-end) #f] + [(let ([c (string-ref source i)]) + (or (identifier-token-char? c) (char=? c #\_))) + (scan (+ i 1) comma?)] + [(char=? (string-ref source i) #\,) (scan (+ i 1) #t)] + [(let ([c (string-ref source i)]) + (or (char=? c #\space) (char=? c #\tab))) + (scan (+ i 1) comma?)] + [(and (char=? (string-ref source i) #\:) + (< (+ i 1) line-end) + (char=? (string-ref source (+ i 1)) #\=)) + (and comma? (go-multi-build source first i (+ i 2) line-end))] + [(and (char=? (string-ref source i) #\=) + (or (>= (+ i 1) line-end) + (not (char=? (string-ref source (+ i 1)) #\=)))) + (and comma? (go-multi-build source first i (+ i 1) line-end))] + [else #f])))) + +(def (go-multi-build source first lhs-end op-end line-end) + (let* ([targets (go-split-lhs-targets + source first (go-line-trimmed-end source first lhs-end))] + [r-start (skip-horizontal-forward source op-end)] + [r-end (go-assignment-rhs-end source r-start line-end)]) + (and (pair? targets) (> r-end r-start) + (list targets r-start r-end)))) + +(def (scan-go-multi-assignment-propagators rule path source) + ;; Go `a, b, _ := f()` propagates taint from the RHS to each named target. + (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-multi-assignment-parts source first line-end)] + [next (if (< line-end len) (+ line-end 1) (+ len 1))]) + (if (not parts) + (loop next acc) + (let ([targets (car parts)] [r-start (cadr parts)] [r-end (caddr parts)]) + (loop next + (append + (reverse + (map (lambda (tgt) + (let* ([l-binding (metavariable-binding-for-range + "L" source (car tgt) (cdr tgt))] + [r-binding (metavariable-binding-for-range + "R" source r-start r-end)] + [finding (finding-for-range-with-bindings + rule path source (car tgt) r-end + (list (cons "L" l-binding) + (cons "R" r-binding)))]) + (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)))) + targets)) + acc))))))))) + (def (scan-go-implicit-assignment-propagators rule path source) (let ([len (string-length source)]) (let loop ([line-start 0] [acc '()]) @@ -33144,7 +33246,8 @@ (if (go-language? language) (append (scan-go-implicit-assignment-propagators rule path source) - (scan-go-range-propagators rule path source)) + (scan-go-range-propagators rule path source) + (scan-go-multi-assignment-propagators rule path source)) '()))] [sanitizers (scan-taint-specs rule