Close go/command-injection via structural matcher (taint 9/9)

ober

08a9470cf04941052b0014861f25f503e2c8d3e8

diff --git a/lib/semgrep/match/structural.sls b/lib/semgrep/match/structural.sls
index d13d92c..75490a4 100644
--- 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)
diff --git a/lib/semgrep/scan.sls b/lib/semgrep/scan.sls
index 2d4e24a..67d7152 100644
--- 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 '())
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index e26c09f..fb1ea87 100644
--- 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"))
diff --git a/src/semgrep/match/structural.ss b/src/semgrep/match/structural.ss
index cb20419..dae8b86 100644
--- 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)
diff --git a/src/semgrep/scan.ss b/src/semgrep/scan.ss
index b979578..251f6b3 100644
--- 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