diff --git a/.github/workflows/ci-rules.yaml b/.github/workflows/ci-rules.yaml index 2ef599626..b0f0704b0 100644 --- a/.github/workflows/ci-rules.yaml +++ b/.github/workflows/ci-rules.yaml @@ -43,6 +43,17 @@ jobs: fileName: opentaint-project-analyzer.jar out-file-path: opentaint-analyzer + - uses: robinraju/release-downloader@v1.12 + with: + repository: ${{ github.repository }} + token: ${{ secrets.SEQRA_GITHUB_TOKEN }} + tag: go-server/latest + fileName: go-ssa-server_linux_amd64 + out-file-path: opentaint-go-server + + - name: Mark go-ssa-server executable + run: chmod +x opentaint-go-server/go-ssa-server_linux_amd64 + - name: Set up JDK 17 uses: actions/setup-java@v4 with: @@ -61,6 +72,11 @@ jobs: SEQRA_GITHUB_ACTOR: ${{ secrets.SEQRA_GITHUB_ACTOR }} SEQRA_GITHUB_TOKEN: ${{ secrets.SEQRA_GITHUB_TOKEN }} + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.25.x' + - name: OpenTaint compile test project run: | java -jar opentaint-autobuilder/opentaint-project-auto-builder.jar \ @@ -71,6 +87,8 @@ jobs: --verbosity debug - name: Run OpenTaint analyzer + env: + GOIR_SERVER_BINARY: opentaint-go-server/go-ssa-server_linux_amd64 run: | java -Xmx8G -Djdk.util.jar.enableMultiRelease=false -Dorg.opentaint.ir.impl.storage.defaultBatchSize=2000 \ -jar opentaint-analyzer/opentaint-project-analyzer.jar \ diff --git a/rules/ruleset/go/lib/go-cmdi-sinks.yaml b/rules/ruleset/go/lib/go-cmdi-sinks.yaml new file mode 100644 index 000000000..0887ab88a --- /dev/null +++ b/rules/ruleset/go/lib/go-cmdi-sinks.yaml @@ -0,0 +1,20 @@ +rules: + - id: go-cmdi-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: OS command execution sink + mode: taint + pattern-sinks: + - pattern-either: + - pattern: exec.Command($NAME, ...) + - pattern: exec.CommandContext($CTX, $NAME, ...) + - pattern: exec.LookPath($NAME) + - pattern: os.StartProcess($NAME, ...) + - pattern: syscall.Exec($NAME, ...) + - pattern: syscall.ForkExec($NAME, ...) + - pattern: syscall.StartProcess($NAME, ...) + - pattern: "($C : *exec.Cmd).CombinedOutput()" + - pattern: "($C : *exec.Cmd).Run()" + - pattern: "($C : *exec.Cmd).Output()" + - pattern: "($C : *exec.Cmd).Start()" diff --git a/rules/ruleset/go/lib/go-path-sinks.yaml b/rules/ruleset/go/lib/go-path-sinks.yaml new file mode 100644 index 000000000..31f305ce9 --- /dev/null +++ b/rules/ruleset/go/lib/go-path-sinks.yaml @@ -0,0 +1,40 @@ +rules: + - id: go-path-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: Filesystem path sink + mode: taint + pattern-sinks: + - pattern-either: + - pattern: os.Open($P) + - pattern: os.OpenFile($P, ...) + - pattern: os.Create($P) + - pattern: os.CreateTemp($DIR, $PAT) + - pattern: os.MkdirTemp($DIR, $PAT) + - pattern: os.Mkdir($P, ...) + - pattern: os.MkdirAll($P, ...) + - pattern: os.Remove($P) + - pattern: os.RemoveAll($P) + - pattern: os.ReadFile($P) + - pattern: os.WriteFile($P, ...) + - pattern: os.ReadDir($P) + - pattern: os.Stat($P) + - pattern: os.Lstat($P) + - pattern: os.Truncate($P, ...) + - pattern: os.Chdir($P) + - pattern: os.Chmod($P, ...) + - pattern: os.Chown($P, ...) + - pattern: os.Lchown($P, ...) + - pattern: os.Chtimes($P, ...) + - pattern: os.Readlink($P) + - pattern: os.Rename($A, $B) + - pattern: os.Link($A, $B) + - pattern: os.Symlink($A, $B) + - pattern: os.DirFS($P) + - pattern: ioutil.ReadFile($P) + - pattern: ioutil.WriteFile($P, ...) + - pattern: ioutil.ReadDir($P) + - pattern: ioutil.TempFile($DIR, $PAT) + - pattern: ioutil.TempDir($DIR, $PAT) + - pattern: http.ServeFile($W, $R, $P) diff --git a/rules/ruleset/go/lib/go-sql-sinks.yaml b/rules/ruleset/go/lib/go-sql-sinks.yaml new file mode 100644 index 000000000..d076ba557 --- /dev/null +++ b/rules/ruleset/go/lib/go-sql-sinks.yaml @@ -0,0 +1,32 @@ +rules: + - id: go-sql-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: SQL query execution sink + mode: taint + pattern-sinks: + - patterns: + - pattern: "($DB : *sql.DB).Query($SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).QueryContext($CTX, $SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).QueryRow($SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).QueryRowContext($CTX, $SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).Exec($SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).ExecContext($CTX, $SQL, ...)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).Prepare($SQL)" + - focus-metavariable: $SQL + - patterns: + - pattern: "($DB : *sql.DB).PrepareContext($CTX, $SQL)" + - focus-metavariable: $SQL diff --git a/rules/ruleset/go/lib/go-ssrf-sinks.yaml b/rules/ruleset/go/lib/go-ssrf-sinks.yaml new file mode 100644 index 000000000..9f905892e --- /dev/null +++ b/rules/ruleset/go/lib/go-ssrf-sinks.yaml @@ -0,0 +1,21 @@ +rules: + - id: go-ssrf-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: Outbound HTTP request sink + mode: taint + pattern-sinks: + - patterns: + - pattern-either: + - pattern: http.Get($U) + - pattern: http.Head($U) + - pattern: http.Post($U, ...) + - pattern: http.PostForm($U, ...) + - pattern: "($C : *http.Client).Get($U)" + - pattern: "($C : *http.Client).Head($U)" + - pattern: "($C : *http.Client).Post($U, ...)" + - pattern: "($C : *http.Client).PostForm($U, ...)" + - pattern: http.NewRequest($M, $U, ...) + - pattern: http.NewRequestWithContext($CTX, $M, $U, ...) + - focus-metavariable: $U diff --git a/rules/ruleset/go/lib/go-ssti-sinks.yaml b/rules/ruleset/go/lib/go-ssti-sinks.yaml new file mode 100644 index 000000000..90b11f589 --- /dev/null +++ b/rules/ruleset/go/lib/go-ssti-sinks.yaml @@ -0,0 +1,16 @@ +rules: + - id: go-ssti-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: Template source parse sink + mode: taint + pattern-sinks: + - patterns: + - pattern-either: + - pattern: template.New($N).Parse($SRC) + - pattern: template.Must(template.New($N).Parse($SRC)) + - pattern: "($T : *template.Template).Parse($SRC)" + - pattern: template.New($N).ParseFiles($SRC, ...) + - pattern: template.New($N).ParseGlob($SRC) + - focus-metavariable: $SRC diff --git a/rules/ruleset/go/lib/go-xss-sinks.yaml b/rules/ruleset/go/lib/go-xss-sinks.yaml new file mode 100644 index 000000000..6946dba16 --- /dev/null +++ b/rules/ruleset/go/lib/go-xss-sinks.yaml @@ -0,0 +1,27 @@ +rules: + - id: go-xss-sinks + options: {lib: true} + languages: [go] + severity: ERROR + message: HTTP response write sink + mode: taint + pattern-sinks: + - pattern-either: + - pattern: $W.Write($BUF) + - pattern: $W.WriteString($S) + - pattern: $O.Body($BODY) + - pattern: fmt.Fprint($W, ...) + - pattern: fmt.Fprintf($W, ...) + - pattern: fmt.Fprintln($W, ...) + - pattern: io.WriteString($W, $S) + - pattern: json.NewEncoder($W).Encode($DATA) + - pattern: $C.ServeJSON() + - pattern: $O.JSON($DATA, ...) + pattern-sanitizers: + - pattern: template.HTMLEscapeString($X) + - pattern: template.JSEscapeString($X) + - pattern: template.URLQueryEscaper($X) + - pattern: template.HTMLEscaper($X) + - pattern: html.EscapeString($X) + - pattern: url.QueryEscape($X) + - pattern: url.PathEscape($X) diff --git a/rules/ruleset/go/lib/http-sources-header-index.yaml b/rules/ruleset/go/lib/http-sources-header-index.yaml new file mode 100644 index 000000000..a24a45b24 --- /dev/null +++ b/rules/ruleset/go/lib/http-sources-header-index.yaml @@ -0,0 +1,10 @@ +rules: + - id: go-http-sources-header-index + options: {lib: true} + languages: [go] + severity: NOTE + message: Untrusted header-map source + mode: taint + pattern-sources: + - label: "$UNTRUSTED" + pattern: $X.Header($K) diff --git a/rules/ruleset/go/lib/http-sources-requesturi.yaml b/rules/ruleset/go/lib/http-sources-requesturi.yaml new file mode 100644 index 000000000..4a3a4f4ed --- /dev/null +++ b/rules/ruleset/go/lib/http-sources-requesturi.yaml @@ -0,0 +1,10 @@ +rules: + - id: go-http-sources-requesturi + options: {lib: true} + languages: [go] + severity: NOTE + message: Untrusted request URI source + mode: taint + pattern-sources: + - label: "$UNTRUSTED" + pattern: $R.RequestURI diff --git a/rules/ruleset/go/lib/http-sources.yaml b/rules/ruleset/go/lib/http-sources.yaml new file mode 100644 index 000000000..c2f9e40b2 --- /dev/null +++ b/rules/ruleset/go/lib/http-sources.yaml @@ -0,0 +1,77 @@ +rules: + - id: go-http-sources + options: {lib: true} + languages: [go] + severity: NOTE + message: Untrusted user input originates here + mode: taint + pattern-sources: + - label: "$UNTRUSTED" + pattern-either: + - pattern: "($R : *http.Request).FormValue($K)" + - pattern: "($R : *http.Request).PostFormValue($K)" + - pattern: "($R : *http.Request).FormFile($K)" + - pattern: "($R : *http.Request).Cookie($K)" + - pattern: "($R : *http.Request).Cookies()" + - pattern: "($R : *http.Request).MultipartReader()" + - pattern: "($R : *http.Request).Referer()" + - pattern: "($R : *http.Request).UserAgent()" + - pattern: $R.URL.Query() + - pattern: $R.URL.Query().Get($K) + - pattern: $R.URL.Path + - pattern: $R.URL.RawQuery + - pattern: $R.URL.RawPath + - pattern: $R.Header.Get($K) + - pattern: $R.Header[$K] + - pattern: $R.Header[$K][$I] + - pattern: $R.Form[$K] + - pattern: $R.Form[$K][$I] + - pattern: $R.PostForm[$K] + - pattern: $R.PostForm[$K][$I] + - pattern: $R.URL.Query()[$K] + - pattern: $R.URL.Query()[$K][$I] + - pattern: $R.Header.Values($K) + - pattern: $R.Form.Get($K) + - pattern: $R.PostForm.Get($K) + - pattern: $R.Body + - pattern: $R.GetBody + - pattern: $R.Form + - pattern: $R.PostForm + - pattern: $R.MultipartForm + - pattern: $R.Header + - pattern: $R.Trailer + - pattern: $R.URL + - pattern: os.Getenv($K) + - pattern: os.LookupEnv($K) + - pattern: os.Args + # beego Controller methods + - pattern: "($C : *web.Controller).GetString($K)" + - pattern: "($C : *web.Controller).GetString($K, $DEF)" + - pattern: "($C : *web.Controller).GetStrings($K)" + - pattern: "($C : *web.Controller).GetStrings($K, $DEF)" + - pattern: $C.GetStrings($K)[$IDX] + - pattern: $C.GetStrings($K, $DEF)[$IDX] + - pattern: "($C : *web.Controller).GetInt($K)" + - pattern: "($C : *web.Controller).GetInt($K, $DEF)" + - pattern: "($C : *web.Controller).GetInt8($K)" + - pattern: "($C : *web.Controller).GetInt16($K)" + - pattern: "($C : *web.Controller).GetInt32($K)" + - pattern: "($C : *web.Controller).GetInt64($K)" + - pattern: "($C : *web.Controller).GetUint8($K)" + - pattern: "($C : *web.Controller).GetUint16($K)" + - pattern: "($C : *web.Controller).GetUint32($K)" + - pattern: "($C : *web.Controller).GetUint64($K)" + - pattern: "($C : *web.Controller).GetFloat($K)" + - pattern: "($C : *web.Controller).GetBool($K)" + - pattern: "($C : *web.Controller).GetFile($K)" + - pattern: "($C : *web.Controller).GetFiles($K)" + - pattern: "($C : *web.Controller).GetSession($K)" + - pattern: "($C : *web.Controller).Input()" + - pattern: "($C : *web.Controller).ParseForm($PTR)" + # beego Context.Input methods (c.Ctx.Input.X) + - pattern: "($X : *context.BeegoInput).Param($K)" + - pattern: "($X : *context.BeegoInput).Params()" + - pattern: "($X : *context.BeegoInput).URI()" + - pattern: "($X : *context.BeegoInput).URL()" + - pattern: $X.RequestBody + - pattern: "($X : *context.BeegoInput).Bind($PTR)" diff --git a/rules/ruleset/go/security/cmdinj.yaml b/rules/ruleset/go/security/cmdinj.yaml new file mode 100644 index 000000000..6b2833a8b --- /dev/null +++ b/rules/ruleset/go/security/cmdinj.yaml @@ -0,0 +1,20 @@ +rules: + - id: go-command-injection + languages: [go] + severity: ERROR + message: Tainted user input reaches OS command execution (command injection) + metadata: + cwe: CWE-78 + short-description: Command injection + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/http-sources-header-index.yaml#go-http-sources-header-index + as: extra_header + - rule: go/lib/go-cmdi-sinks.yaml#go-cmdi-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' + - 'extra_header.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/ruleset/go/security/path-traversal.yaml b/rules/ruleset/go/security/path-traversal.yaml new file mode 100644 index 000000000..542b79b75 --- /dev/null +++ b/rules/ruleset/go/security/path-traversal.yaml @@ -0,0 +1,17 @@ +rules: + - id: go-path-traversal + languages: [go] + severity: ERROR + message: Tainted user input reaches filesystem path (path traversal) + metadata: + cwe: CWE-22 + short-description: Path traversal + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/go-path-sinks.yaml#go-path-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/ruleset/go/security/sql-injection.yaml b/rules/ruleset/go/security/sql-injection.yaml new file mode 100644 index 000000000..813decb93 --- /dev/null +++ b/rules/ruleset/go/security/sql-injection.yaml @@ -0,0 +1,23 @@ +rules: + - id: go-sql-injection + languages: [go] + severity: ERROR + message: Tainted user input flows into SQL query (SQL injection) + metadata: + cwe: CWE-89 + short-description: SQL injection + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/http-sources-requesturi.yaml#go-http-sources-requesturi + as: extra_requesturi + - rule: go/lib/http-sources-header-index.yaml#go-http-sources-header-index + as: extra_header + - rule: go/lib/go-sql-sinks.yaml#go-sql-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' + - 'extra_requesturi.$UNTRUSTED -> sink.$UNTRUSTED' + - 'extra_header.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/ruleset/go/security/ssrf.yaml b/rules/ruleset/go/security/ssrf.yaml new file mode 100644 index 000000000..b51154e39 --- /dev/null +++ b/rules/ruleset/go/security/ssrf.yaml @@ -0,0 +1,17 @@ +rules: + - id: go-ssrf + languages: [go] + severity: ERROR + message: Tainted user input flows into a server-side HTTP request (SSRF) + metadata: + cwe: CWE-918 + short-description: Server-side request forgery + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/go-ssrf-sinks.yaml#go-ssrf-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/ruleset/go/security/ssti.yaml b/rules/ruleset/go/security/ssti.yaml new file mode 100644 index 000000000..037306039 --- /dev/null +++ b/rules/ruleset/go/security/ssti.yaml @@ -0,0 +1,17 @@ +rules: + - id: go-ssti + languages: [go] + severity: ERROR + message: Tainted user input parsed as a template source (server-side template injection) + metadata: + cwe: CWE-1336 + short-description: Server-side template injection + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/go-ssti-sinks.yaml#go-ssti-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/ruleset/go/security/trust-boundary.yaml b/rules/ruleset/go/security/trust-boundary.yaml new file mode 100644 index 000000000..87619adc9 --- /dev/null +++ b/rules/ruleset/go/security/trust-boundary.yaml @@ -0,0 +1,84 @@ +rules: + - id: go-trust-boundary-cwe-501 + languages: [go] + severity: ERROR + message: Tainted user input stored across a trust boundary (cookie/header/session) + metadata: + cwe: CWE-501 + short-description: Trust boundary violation + mode: taint + pattern-sources: + - pattern-either: + - pattern: "($R : *http.Request).FormValue($K)" + - pattern: "($R : *http.Request).PostFormValue($K)" + - pattern: "($R : *http.Request).FormFile($K)" + - pattern: "($R : *http.Request).Cookie($K)" + - pattern: "($R : *http.Request).Cookies()" + - pattern: "($R : *http.Request).MultipartReader()" + - pattern: "($R : *http.Request).Referer()" + - pattern: "($R : *http.Request).UserAgent()" + - pattern: $R.URL.Query() + - pattern: $R.URL.Query().Get($K) + - pattern: $R.URL.Path + - pattern: $R.URL.RawQuery + - pattern: $R.URL.RawPath + - pattern: $R.RequestURI + - pattern: $R.Header.Get($K) + - pattern: $R.Header[$K] + - pattern: $R.Header[$K][$I] + - pattern: $R.Form[$K] + - pattern: $R.Form[$K][$I] + - pattern: $R.PostForm[$K] + - pattern: $R.PostForm[$K][$I] + - pattern: $R.URL.Query()[$K] + - pattern: $R.URL.Query()[$K][$I] + - pattern: $R.Header.Values($K) + - pattern: $R.Form.Get($K) + - pattern: $R.PostForm.Get($K) + - pattern: $R.Body + - pattern: $R.GetBody + - pattern: $R.Form + - pattern: $R.PostForm + - pattern: $R.MultipartForm + - pattern: $R.Header + - pattern: $R.Trailer + - pattern: $R.URL + - pattern: os.Getenv($K) + - pattern: os.LookupEnv($K) + - pattern: os.Args + # beego Controller methods + - pattern: "($C : *web.Controller).GetString($K)" + - pattern: "($C : *web.Controller).GetString($K, $DEF)" + - pattern: "($C : *web.Controller).GetStrings($K)" + - pattern: "($C : *web.Controller).GetStrings($K, $DEF)" + - pattern: $C.GetStrings($K)[$IDX] + - pattern: $C.GetStrings($K, $DEF)[$IDX] + - pattern: "($C : *web.Controller).GetInt($K)" + - pattern: "($C : *web.Controller).GetInt($K, $DEF)" + - pattern: "($C : *web.Controller).GetInt8($K)" + - pattern: "($C : *web.Controller).GetInt16($K)" + - pattern: "($C : *web.Controller).GetInt32($K)" + - pattern: "($C : *web.Controller).GetInt64($K)" + - pattern: "($C : *web.Controller).GetUint8($K)" + - pattern: "($C : *web.Controller).GetUint16($K)" + - pattern: "($C : *web.Controller).GetUint32($K)" + - pattern: "($C : *web.Controller).GetUint64($K)" + - pattern: "($C : *web.Controller).GetFloat($K)" + - pattern: "($C : *web.Controller).GetBool($K)" + - pattern: "($C : *web.Controller).GetFile($K)" + - pattern: "($C : *web.Controller).GetFiles($K)" + - pattern: "($C : *web.Controller).GetSession($K)" + - pattern: "($C : *web.Controller).Input()" + - pattern: "($C : *web.Controller).ParseForm($PTR)" + # beego Context.Input methods (c.Ctx.Input.X) + - pattern: "($X : *context.BeegoInput).Param($K)" + - pattern: $X.Header($K) + - pattern: "($X : *context.BeegoInput).Params()" + - pattern: "($X : *context.BeegoInput).URI()" + - pattern: "($X : *context.BeegoInput).URL()" + - pattern: $X.RequestBody + - pattern: "($X : *context.BeegoInput).Bind($PTR)" + pattern-sinks: + - pattern-either: + - pattern: http.SetCookie($W, $C) + - pattern: $C.SetSession($K, $V) diff --git a/rules/ruleset/go/security/weak-crypto.yaml b/rules/ruleset/go/security/weak-crypto.yaml new file mode 100644 index 000000000..0011fda3a --- /dev/null +++ b/rules/ruleset/go/security/weak-crypto.yaml @@ -0,0 +1,18 @@ +rules: + - id: go-weak-crypto-cwe-327 + languages: [go] + severity: ERROR + message: Use of a broken or risky cryptographic algorithm + metadata: + cwe: CWE-327 + short-description: Broken or risky crypto algorithm + mode: search + pattern-either: + - pattern: des.NewCipher(...) + - pattern: des.NewTripleDESCipher(...) + - pattern: rc4.NewCipher(...) + - pattern: md4.New(...) + - pattern: cipher.NewECBEncrypter(...) + - pattern: cipher.NewECBDecrypter(...) + - pattern: NewECBEncrypter(...) + - pattern: NewECBDecrypter(...) diff --git a/rules/ruleset/go/security/weak-hash.yaml b/rules/ruleset/go/security/weak-hash.yaml new file mode 100644 index 000000000..2850db125 --- /dev/null +++ b/rules/ruleset/go/security/weak-hash.yaml @@ -0,0 +1,15 @@ +rules: + - id: go-weak-hash-cwe-328 + languages: [go] + severity: ERROR + message: Use of a weak or broken cryptographic hash + metadata: + cwe: CWE-328 + short-description: Weak cryptographic hash + mode: search + pattern-either: + - pattern: md5.New(...) + - pattern: md5.Sum(...) + - pattern: sha1.New(...) + - pattern: sha1.Sum(...) + - pattern: md4.New(...) diff --git a/rules/ruleset/go/security/weak-random.yaml b/rules/ruleset/go/security/weak-random.yaml new file mode 100644 index 000000000..e2d939265 --- /dev/null +++ b/rules/ruleset/go/security/weak-random.yaml @@ -0,0 +1,24 @@ +rules: + - id: go-weak-random-cwe-330 + languages: [go] + severity: ERROR + message: Use of a weak (non-cryptographic) random number generator + metadata: + cwe: CWE-330 + short-description: Insecure randomness + mode: search + pattern-either: + - pattern: rand.Int() + - pattern: rand.Intn(...) + - pattern: rand.Int31() + - pattern: rand.Int31n(...) + - pattern: rand.Int63() + - pattern: rand.Int63n(...) + - pattern: rand.Float64() + - pattern: rand.Float32() + - pattern: rand.NormFloat64() + - pattern: rand.ExpFloat64() + - pattern: rand.Uint32() + - pattern: rand.Uint64() + - pattern: rand.Perm(...) + - pattern: rand.New(...) diff --git a/rules/ruleset/go/security/xss.yaml b/rules/ruleset/go/security/xss.yaml new file mode 100644 index 000000000..859039f7e --- /dev/null +++ b/rules/ruleset/go/security/xss.yaml @@ -0,0 +1,17 @@ +rules: + - id: go-reflected-xss + languages: [go] + severity: ERROR + message: Tainted user input written to HTTP response body (reflected XSS) + metadata: + cwe: CWE-79 + short-description: Reflected cross-site scripting + mode: join + join: + refs: + - rule: go/lib/http-sources.yaml#go-http-sources + as: src + - rule: go/lib/go-xss-sinks.yaml#go-xss-sinks + as: sink + on: + - 'src.$UNTRUSTED -> sink.$UNTRUSTED' diff --git a/rules/test/go/go.mod b/rules/test/go/go.mod new file mode 100644 index 000000000..ea8529a81 --- /dev/null +++ b/rules/test/go/go.mod @@ -0,0 +1,7 @@ +module test + +go 1.18 + +require github.com/beego/beego/v2 v2.0.0 + +replace github.com/beego/beego/v2 => ./stubs/beego diff --git a/rules/test/go/internal/md4/md4.go b/rules/test/go/internal/md4/md4.go new file mode 100644 index 000000000..5c85c117a --- /dev/null +++ b/rules/test/go/internal/md4/md4.go @@ -0,0 +1,7 @@ +package md4 + +type Digest struct{} + +func New(args ...interface{}) *Digest { + return &Digest{} +} diff --git a/rules/test/go/internal/weakcipher/weakcipher.go b/rules/test/go/internal/weakcipher/weakcipher.go new file mode 100644 index 000000000..87a1957ce --- /dev/null +++ b/rules/test/go/internal/weakcipher/weakcipher.go @@ -0,0 +1,11 @@ +package weakcipher + +type blockMode struct{} + +func NewECBEncrypter(args ...interface{}) *blockMode { + return &blockMode{} +} + +func NewECBDecrypter(args ...interface{}) *blockMode { + return &blockMode{} +} diff --git a/rules/test/go/rule-test.yaml b/rules/test/go/rule-test.yaml new file mode 100644 index 000000000..4a3392454 --- /dev/null +++ b/rules/test/go/rule-test.yaml @@ -0,0 +1,213 @@ +tests: + - rule-id: go/security/sql-injection.yaml#go-sql-injection + positive: + - test/security/all-patterns.PositiveSourceRequestFormValue + - test/security/all-patterns.PositiveSourceRequestPostFormValue + - test/security/all-patterns.PositiveSourceRequestCookies + - test/security/all-patterns.PositiveSourceRequestReferer + - test/security/all-patterns.PositiveSourceRequestUserAgent + - test/security/all-patterns.PositiveSourceURLQuery + - test/security/all-patterns.PositiveSourceURLQueryGet + - test/security/all-patterns.PositiveSourceURLPath + - test/security/all-patterns.PositiveSourceURLRawQuery + - test/security/all-patterns.PositiveSourceURLRawPath + - test/security/all-patterns.PositiveSourceHeaderGet + - test/security/all-patterns.PositiveSourceHeaderMap + - test/security/all-patterns.PositiveSourceHeaderMapIndex + - test/security/all-patterns.PositiveSourceFormMap + - test/security/all-patterns.PositiveSourceFormMapIndex + - test/security/all-patterns.PositiveSourcePostFormMap + - test/security/all-patterns.PositiveSourcePostFormMapIndex + - test/security/all-patterns.PositiveSourceURLQueryMap + - test/security/all-patterns.PositiveSourceURLQueryMapIndex + - test/security/all-patterns.PositiveSourceHeaderValues + - test/security/all-patterns.PositiveSourceFormGet + - test/security/all-patterns.PositiveSourcePostFormGet + - test/security/all-patterns.PositiveSourceBody + - test/security/all-patterns.PositiveSourceGetBody + - test/security/all-patterns.PositiveSourceForm + - test/security/all-patterns.PositiveSourcePostForm + - test/security/all-patterns.PositiveSourceMultipartForm + - test/security/all-patterns.PositiveSourceHeader + - test/security/all-patterns.PositiveSourceTrailer + - test/security/all-patterns.PositiveSourceURL + - test/security/all-patterns.PositiveSourceGetenv + - test/security/all-patterns.PositiveSourceArgs + - test/security/all-patterns.PositiveSourceRequestURI + - test/security/all-patterns.PositiveSourceBeegoGetString + - test/security/all-patterns.PositiveSourceBeegoGetStringDefault + - test/security/all-patterns.PositiveSourceBeegoGetStrings + - test/security/all-patterns.PositiveSourceBeegoGetStringsDefault + - test/security/all-patterns.PositiveSourceBeegoGetStringsIndex + - test/security/all-patterns.PositiveSourceBeegoGetStringsDefaultIndex + - test/security/all-patterns.PositiveSourceBeegoGetSession + - test/security/all-patterns.PositiveSourceBeegoInput + - test/security/all-patterns.PositiveSourceBeegoInputParam + - test/security/all-patterns.PositiveSourceBeegoInputParams + - test/security/all-patterns.PositiveSourceBeegoInputURI + - test/security/all-patterns.PositiveSourceBeegoInputURL + - test/security/all-patterns.PositiveSourceBeegoInputRequestBody + - test/security/all-patterns.PositiveSourceHeaderMethod + - test/security/all-patterns.PositiveSQLDBQuery + - test/security/all-patterns.PositiveSQLDBQueryContext + - test/security/all-patterns.PositiveSQLDBQueryRow + - test/security/all-patterns.PositiveSQLDBQueryRowContext + - test/security/all-patterns.PositiveSQLDBExec + - test/security/all-patterns.PositiveSQLDBExecContext + - test/security/all-patterns.PositiveSQLDBPrepare + - test/security/all-patterns.PositiveSQLDBPrepareContext + - test/security/all-patterns.PositiveSQLHelperReturnFlow + - test/security/all-patterns.PositiveSQLStructFieldFlow + - test/security/all-patterns.PositiveSQLInterfaceDispatchFlow + negative: + - test/security/all-patterns.NegativeSQLParameterizedArgument + + - rule-id: go/security/cmdinj.yaml#go-command-injection + positive: + - test/security/all-patterns.PositiveCmdExecCommand + - test/security/all-patterns.PositiveCmdExecCommandContext + - test/security/all-patterns.PositiveCmdExecLookPath + - test/security/all-patterns.PositiveCmdOSStartProcess + - test/security/all-patterns.PositiveCmdSyscallExec + - test/security/all-patterns.PositiveCmdSyscallForkExec + - test/security/all-patterns.PositiveCmdSyscallStartProcess + - test/security/all-patterns.PositiveCmdCombinedOutput + - test/security/all-patterns.PositiveCmdRun + - test/security/all-patterns.PositiveCmdOutput + - test/security/all-patterns.PositiveCmdStart + - test/security/all-patterns.PositiveCmdHelperReturnFlow + - test/security/all-patterns.PositiveCmdStructFieldFlow + - test/security/all-patterns.PositiveCmdInterfaceDispatchFlow + + - rule-id: go/security/path-traversal.yaml#go-path-traversal + positive: + - test/security/all-patterns.PositivePathOSOpen + - test/security/all-patterns.PositivePathOSOpenFile + - test/security/all-patterns.PositivePathOSCreate + - test/security/all-patterns.PositivePathOSCreateTempDir + - test/security/all-patterns.PositivePathOSCreateTempPattern + - test/security/all-patterns.PositivePathOSMkdirTempDir + - test/security/all-patterns.PositivePathOSMkdirTempPattern + - test/security/all-patterns.PositivePathOSMkdir + - test/security/all-patterns.PositivePathOSMkdirAll + - test/security/all-patterns.PositivePathOSRemove + - test/security/all-patterns.PositivePathOSRemoveAll + - test/security/all-patterns.PositivePathOSReadFile + - test/security/all-patterns.PositivePathOSWriteFile + - test/security/all-patterns.PositivePathOSReadDir + - test/security/all-patterns.PositivePathOSStat + - test/security/all-patterns.PositivePathOSLstat + - test/security/all-patterns.PositivePathOSTruncate + - test/security/all-patterns.PositivePathOSChdir + - test/security/all-patterns.PositivePathOSChmod + - test/security/all-patterns.PositivePathOSChown + - test/security/all-patterns.PositivePathOSLchown + - test/security/all-patterns.PositivePathOSChtimes + - test/security/all-patterns.PositivePathOSReadlink + - test/security/all-patterns.PositivePathOSRenameSource + - test/security/all-patterns.PositivePathOSRenameTarget + - test/security/all-patterns.PositivePathOSLinkSource + - test/security/all-patterns.PositivePathOSLinkTarget + - test/security/all-patterns.PositivePathOSSymlinkSource + - test/security/all-patterns.PositivePathOSSymlinkTarget + - test/security/all-patterns.PositivePathOSDirFS + - test/security/all-patterns.PositivePathIoutilReadFile + - test/security/all-patterns.PositivePathIoutilWriteFile + - test/security/all-patterns.PositivePathIoutilReadDir + - test/security/all-patterns.PositivePathIoutilTempFileDir + - test/security/all-patterns.PositivePathIoutilTempFilePattern + - test/security/all-patterns.PositivePathIoutilTempDirDir + - test/security/all-patterns.PositivePathIoutilTempDirPattern + - test/security/all-patterns.PositivePathHTTPServeFile + - test/security/all-patterns.PositivePathHelperReturnFlow + - test/security/all-patterns.PositivePathStructFieldFlow + - test/security/all-patterns.PositivePathInterfaceDispatchFlow + + - rule-id: go/security/ssrf.yaml#go-ssrf + positive: + - test/security/all-patterns.PositiveSSRFHTTPGet + - test/security/all-patterns.PositiveSSRFHTTPHead + - test/security/all-patterns.PositiveSSRFHTTPPost + - test/security/all-patterns.PositiveSSRFHTTPPostForm + - test/security/all-patterns.PositiveSSRFClientGet + - test/security/all-patterns.PositiveSSRFClientHead + - test/security/all-patterns.PositiveSSRFClientPost + - test/security/all-patterns.PositiveSSRFClientPostForm + - test/security/all-patterns.PositiveSSRFNewRequest + - test/security/all-patterns.PositiveSSRFNewRequestWithContext + - test/security/all-patterns.PositiveSSRFHelperReturnFlow + - test/security/all-patterns.PositiveSSRFChannelFlow + + - rule-id: go/security/ssti.yaml#go-ssti + positive: + - test/security/all-patterns.PositiveSSTITemplateParse + - test/security/all-patterns.PositiveSSTITemplateMustParse + - test/security/all-patterns.PositiveSSTITemplateParseFiles + - test/security/all-patterns.PositiveSSTITemplateParseGlob + - test/security/all-patterns.PositiveSSTIBuilderChainFlow + negative: + - test/security/all-patterns.NegativeSSTIConstantTemplateTaintedData + + - rule-id: go/security/xss.yaml#go-reflected-xss + positive: + - test/security/all-patterns.PositiveXSSWrite + - test/security/all-patterns.PositiveXSSWriteString + - test/security/all-patterns.PositiveXSSBody + - test/security/all-patterns.PositiveXSSFprint + - test/security/all-patterns.PositiveXSSFprintf + - test/security/all-patterns.PositiveXSSFprintln + - test/security/all-patterns.PositiveXSSIOWriteString + - test/security/all-patterns.PositiveXSSJSONEncoderEncode + - test/security/all-patterns.PositiveXSSServeJSON + - test/security/all-patterns.PositiveXSSOutputJSON + - test/security/all-patterns.PositiveXSSHelperReturnFlow + - test/security/all-patterns.PositiveXSSChannelFlow + negative: + - test/security/all-patterns.NegativeXSSHTMLEscapeString + - test/security/all-patterns.NegativeXSSJSEscapeString + - test/security/all-patterns.NegativeXSSHTMLEscape + - test/security/all-patterns.NegativeXSSURLQueryEscape + - test/security/all-patterns.NegativeXSSURLPathEscape + + - rule-id: go/security/trust-boundary.yaml#go-trust-boundary-cwe-501 + positive: + - test/security/all-patterns.PositiveTrustSetCookie + - test/security/all-patterns.PositiveTrustSetSession + - test/security/all-patterns.PositiveTrustSourceBeegoInputHeader + - test/security/all-patterns.PositiveTrustCookiePathFlow + + - rule-id: go/security/weak-crypto.yaml#go-weak-crypto-cwe-327 + positive: + - test/security/all-patterns.PositiveWeakCryptoDES + - test/security/all-patterns.PositiveWeakCryptoTripleDES + - test/security/all-patterns.PositiveWeakCryptoRC4 + - test/security/all-patterns.PositiveWeakCryptoMD4 + - test/security/all-patterns.PositiveWeakCryptoCipherECBEncrypter + - test/security/all-patterns.PositiveWeakCryptoCipherECBDecrypter + - test/security/all-patterns.PositiveWeakCryptoECBEncrypter + - test/security/all-patterns.PositiveWeakCryptoECBDecrypter + + - rule-id: go/security/weak-hash.yaml#go-weak-hash-cwe-328 + positive: + - test/security/all-patterns.PositiveWeakHashMD5New + - test/security/all-patterns.PositiveWeakHashMD5Sum + - test/security/all-patterns.PositiveWeakHashSHA1New + - test/security/all-patterns.PositiveWeakHashSHA1Sum + - test/security/all-patterns.PositiveWeakHashMD4New + + - rule-id: go/security/weak-random.yaml#go-weak-random-cwe-330 + positive: + - test/security/all-patterns.PositiveWeakRandomInt + - test/security/all-patterns.PositiveWeakRandomIntn + - test/security/all-patterns.PositiveWeakRandomInt31 + - test/security/all-patterns.PositiveWeakRandomInt31n + - test/security/all-patterns.PositiveWeakRandomInt63 + - test/security/all-patterns.PositiveWeakRandomInt63n + - test/security/all-patterns.PositiveWeakRandomFloat64 + - test/security/all-patterns.PositiveWeakRandomFloat32 + - test/security/all-patterns.PositiveWeakRandomNormFloat64 + - test/security/all-patterns.PositiveWeakRandomExpFloat64 + - test/security/all-patterns.PositiveWeakRandomUint32 + - test/security/all-patterns.PositiveWeakRandomUint64 + - test/security/all-patterns.PositiveWeakRandomPerm + - test/security/all-patterns.PositiveWeakRandomNew diff --git a/rules/test/go/security/all-patterns/flow_samples.go b/rules/test/go/security/all-patterns/flow_samples.go new file mode 100644 index 000000000..c90da8b22 --- /dev/null +++ b/rules/test/go/security/all-patterns/flow_samples.go @@ -0,0 +1,170 @@ +package allpatterns + +import ( + "bytes" + "html/template" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type sqlClauseBuilder interface { + where(string) string +} + +type prefixClauseBuilder struct{} + +func (prefixClauseBuilder) where(value string) string { + return "name = '" + value + "'" +} + +type queryHolder struct { + where string +} + +func buildSQLWhere(value string) string { + return "name = '" + value + "'" +} + +func PositiveSQLHelperReturnFlow() { + r := requestForSources() + _, _ = db.Query("select * from users where " + buildSQLWhere(r.Header.Get("X-Test"))) +} + +func PositiveSQLStructFieldFlow() { + r := requestForSources() + holder := queryHolder{where: r.URL.Query().Get("q")} + _, _ = db.Query("select * from users where name = '" + holder.where + "'") +} + +func PositiveSQLInterfaceDispatchFlow() { + r := requestForSources() + var builder sqlClauseBuilder = prefixClauseBuilder{} + _, _ = db.Query("select * from users where " + builder.where(r.FormValue("q"))) +} + +func NegativeSQLParameterizedArgument() { + _, _ = db.Query("select * from users where name = ?", envSource()) +} + +type commandJob struct { + name string +} + +type commandBuilder interface { + build(string) string +} + +type shellCommandBuilder struct{} + +func (shellCommandBuilder) build(value string) string { + return value +} + +func commandName(value string) string { + return value +} + +func PositiveCmdHelperReturnFlow() { + r := requestForSources() + _ = exec.Command(commandName(r.URL.Query().Get("cmd"))).Run() +} + +func PositiveCmdStructFieldFlow() { + r := requestForSources() + job := commandJob{name: r.Header.Get("X-Test")} + _, _ = exec.LookPath(job.name) +} + +func PositiveCmdInterfaceDispatchFlow() { + r := requestForSources() + var builder commandBuilder = shellCommandBuilder{} + _ = exec.Command(builder.build(r.FormValue("cmd"))).Start() +} + +type requestedFile struct { + name string +} + +type pathResolver interface { + resolve(string) string +} + +type joinResolver struct{} + +func (joinResolver) resolve(value string) string { + return filepath.Join("static", value) +} + +func resolvePath(value string) string { + return filepath.Join("static", value) +} + +func PositivePathHelperReturnFlow() { + r := requestForSources() + _, _ = os.Open(resolvePath(r.URL.Query().Get("file"))) +} + +func PositivePathStructFieldFlow() { + r := requestForSources() + file := requestedFile{name: r.Header.Get("X-Test")} + _, _ = os.ReadFile(file.name) +} + +func PositivePathInterfaceDispatchFlow() { + r := requestForSources() + var resolver pathResolver = joinResolver{} + _, _ = os.Stat(resolver.resolve(r.FormValue("file"))) +} + +func ssrfTarget(value string) string { + return "https://" + value +} + +func PositiveSSRFHelperReturnFlow() { + r := requestForSources() + _, _ = http.Get(ssrfTarget(r.URL.Query().Get("host"))) +} + +func PositiveSSRFChannelFlow() { + r := requestForSources() + ch := make(chan string, 1) + ch <- r.Header.Get("X-Test") + target := <-ch + _, _ = client.Get("https://" + target) +} + +func xssBody(value string) string { + return "
" + value + "
" +} + +func PositiveXSSHelperReturnFlow() { + r := requestForSources() + _, _ = responseWriterWithString{}.WriteString(xssBody(r.URL.Query().Get("q"))) +} + +func PositiveXSSChannelFlow() { + r := requestForSources() + ch := make(chan string, 1) + ch <- r.FormValue("q") + value := <-ch + _, _ = responseWriterWithString{}.Write([]byte(value)) +} + +func PositiveSSTIBuilderChainFlow() { + r := requestForSources() + t := template.New("x").Funcs(template.FuncMap{"trim": strings.TrimSpace}) + _, _ = t.Parse(r.FormValue("template")) +} + +func NegativeSSTIConstantTemplateTaintedData() { + t := template.Must(template.New("x").Parse("hello {{.}}")) + _ = t.Execute(&bytes.Buffer{}, envSource()) +} + +func PositiveTrustCookiePathFlow() { + r := requestForSources() + http.SetCookie(responseWriterWithString{}, &http.Cookie{Name: "session", Path: r.URL.Path}) +} diff --git a/rules/test/go/security/all-patterns/sample.go b/rules/test/go/security/all-patterns/sample.go new file mode 100644 index 000000000..4fd1baa8a --- /dev/null +++ b/rules/test/go/security/all-patterns/sample.go @@ -0,0 +1,406 @@ +package allpatterns + +import ( + "context" + "crypto/des" + "crypto/md5" + "crypto/rc4" + "crypto/sha1" + "database/sql" + "encoding/json" + "fmt" + "html" + "html/template" + "io" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "os" + "os/exec" + "strings" + "syscall" + "time" + + "github.com/beego/beego/v2/server/web" + beegocontext "github.com/beego/beego/v2/server/web/context" + + md4 "test/internal/md4" + cipher "test/internal/weakcipher" +) + +var ( + db *sql.DB + client = &http.Client{} + ctx = context.Background() +) + +type responseWriterWithString struct{} + +func (responseWriterWithString) Header() http.Header { return http.Header{} } +func (responseWriterWithString) Write(p []byte) (int, error) { return len(p), nil } +func (responseWriterWithString) WriteHeader(statusCode int) {} +func (responseWriterWithString) WriteString(s string) (int, error) { return len(s), nil } + +type beegoOutput struct{} + +func (beegoOutput) Body(body string) {} +func (beegoOutput) JSON(data interface{}, args ...interface{}) {} + +type jsonController struct { + Data interface{} +} + +func (c *jsonController) ServeJSON() {} + +func requestForSources() *http.Request { + r := &http.Request{ + URL: &url.URL{Path: "/path", RawPath: "/raw-path", RawQuery: "q=v"}, + Header: http.Header{"X-Test": []string{"header"}}, + Trailer: http.Header{"X-Test": []string{"trailer"}}, + Form: url.Values{"q": []string{"form"}}, + PostForm: url.Values{"q": []string{"post"}}, + Body: io.NopCloser(strings.NewReader("body")), + RemoteAddr: "127.0.0.1:1", + } + r.RequestURI = "/path?q=v" + r.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader("body")), nil + } + return r +} + +func sqlSink(value interface{}) { + _, _ = db.Query(fmt.Sprint(value)) +} + +func sqlSinkMany(value ...interface{}) { + _, _ = db.Query(fmt.Sprint(value...)) +} + +func envSource() string { + return os.Getenv("TAINTED") +} + +func cookieFrom(value string) *http.Cookie { + return &http.Cookie{Name: "session", Value: value} +} + +func PositiveSourceRequestFormValue() { r := requestForSources(); sqlSink(r.FormValue("q")) } +func PositiveSourceRequestPostFormValue() { r := requestForSources(); sqlSink(r.PostFormValue("q")) } +func UnsupportedPositiveSourceRequestFormFile() { + r := requestForSources() + f, _, _ := r.FormFile("file") + sqlSink(f) +} +func UnsupportedPositiveSourceRequestCookie() { + r := requestForSources() + c, _ := r.Cookie("q") + sqlSink(c) +} +func PositiveSourceRequestCookies() { r := requestForSources(); sqlSink(r.Cookies()) } +func UnsupportedPositiveSourceRequestMultipartReader() { + r := requestForSources() + mr, _ := r.MultipartReader() + sqlSink(mr) +} +func PositiveSourceRequestReferer() { r := requestForSources(); sqlSink(r.Referer()) } +func PositiveSourceRequestUserAgent() { r := requestForSources(); sqlSink(r.UserAgent()) } +func PositiveSourceURLQuery() { r := requestForSources(); sqlSink(r.URL.Query()) } +func PositiveSourceURLQueryGet() { r := requestForSources(); sqlSink(r.URL.Query().Get("q")) } +func PositiveSourceURLPath() { r := requestForSources(); sqlSink(r.URL.Path) } +func PositiveSourceURLRawQuery() { r := requestForSources(); sqlSink(r.URL.RawQuery) } +func PositiveSourceURLRawPath() { r := requestForSources(); sqlSink(r.URL.RawPath) } +func PositiveSourceHeaderGet() { r := requestForSources(); sqlSink(r.Header.Get("X-Test")) } +func PositiveSourceHeaderMap() { r := requestForSources(); sqlSink(r.Header["X-Test"]) } +func PositiveSourceHeaderMapIndex() { r := requestForSources(); sqlSink(r.Header["X-Test"][0]) } +func PositiveSourceFormMap() { r := requestForSources(); sqlSink(r.Form["q"]) } +func PositiveSourceFormMapIndex() { r := requestForSources(); sqlSink(r.Form["q"][0]) } +func PositiveSourcePostFormMap() { r := requestForSources(); sqlSink(r.PostForm["q"]) } +func PositiveSourcePostFormMapIndex() { r := requestForSources(); sqlSink(r.PostForm["q"][0]) } +func PositiveSourceURLQueryMap() { r := requestForSources(); sqlSink(r.URL.Query()["q"]) } +func PositiveSourceURLQueryMapIndex() { r := requestForSources(); sqlSink(r.URL.Query()["q"][0]) } +func PositiveSourceHeaderValues() { r := requestForSources(); sqlSink(r.Header.Values("X-Test")) } +func PositiveSourceFormGet() { r := requestForSources(); sqlSink(r.Form.Get("q")) } +func PositiveSourcePostFormGet() { r := requestForSources(); sqlSink(r.PostForm.Get("q")) } +func PositiveSourceBody() { r := requestForSources(); sqlSink(r.Body) } +func PositiveSourceGetBody() { r := requestForSources(); sqlSink(r.GetBody) } +func PositiveSourceForm() { r := requestForSources(); sqlSink(r.Form) } +func PositiveSourcePostForm() { r := requestForSources(); sqlSink(r.PostForm) } +func PositiveSourceMultipartForm() { r := requestForSources(); sqlSink(r.MultipartForm) } +func PositiveSourceHeader() { r := requestForSources(); sqlSink(r.Header) } +func PositiveSourceTrailer() { r := requestForSources(); sqlSink(r.Trailer) } +func PositiveSourceURL() { r := requestForSources(); sqlSink(r.URL) } +func PositiveSourceGetenv() { sqlSink(os.Getenv("TAINTED")) } +func UnsupportedPositiveSourceLookupEnv() { + v, _ := os.LookupEnv("TAINTED") + sqlSink(v) +} +func PositiveSourceArgs() { sqlSink(os.Args) } +func PositiveSourceRequestURI() { r := requestForSources(); sqlSink(r.RequestURI) } + +func PositiveSourceBeegoGetString() { c := &web.Controller{}; sqlSink(c.GetString("q")) } +func PositiveSourceBeegoGetStringDefault() { c := &web.Controller{}; sqlSink(c.GetString("q", "d")) } +func PositiveSourceBeegoGetStrings() { c := &web.Controller{}; sqlSink(c.GetStrings("q")) } +func PositiveSourceBeegoGetStringsDefault() { c := &web.Controller{}; sqlSink(c.GetStrings("q", "d")) } +func PositiveSourceBeegoGetStringsIndex() { c := &web.Controller{}; sqlSink(c.GetStrings("q")[0]) } +func PositiveSourceBeegoGetStringsDefaultIndex() { + c := &web.Controller{} + sqlSink(c.GetStrings("q", "d")[0]) +} +func UnsupportedPositiveSourceBeegoGetInt() { + c := &web.Controller{} + v, _ := c.GetInt("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetIntDefault() { + c := &web.Controller{} + v, _ := c.GetInt("q", 1) + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetInt8() { + c := &web.Controller{} + v, _ := c.GetInt8("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetInt16() { + c := &web.Controller{} + v, _ := c.GetInt16("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetInt32() { + c := &web.Controller{} + v, _ := c.GetInt32("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetInt64() { + c := &web.Controller{} + v, _ := c.GetInt64("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetUint8() { + c := &web.Controller{} + v, _ := c.GetUint8("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetUint16() { + c := &web.Controller{} + v, _ := c.GetUint16("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetUint32() { + c := &web.Controller{} + v, _ := c.GetUint32("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetUint64() { + c := &web.Controller{} + v, _ := c.GetUint64("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetFloat() { + c := &web.Controller{} + v, _ := c.GetFloat("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetBool() { + c := &web.Controller{} + v, _ := c.GetBool("q") + sqlSink(v) +} +func UnsupportedPositiveSourceBeegoGetFile() { + c := &web.Controller{} + f, _, _ := c.GetFile("q") + sqlSink(f) +} +func UnsupportedPositiveSourceBeegoGetFiles() { + c := &web.Controller{} + f, _ := c.GetFiles("q") + sqlSink(f) +} +func PositiveSourceBeegoGetSession() { c := &web.Controller{}; sqlSink(c.GetSession("q")) } +func PositiveSourceBeegoInput() { c := &web.Controller{}; sqlSink(c.Input()) } +func UnsupportedPositiveSourceBeegoParseForm() { + c := &web.Controller{} + var dst struct{ Q string } + sqlSinkMany(c.ParseForm(&dst)) +} + +func PositiveSourceBeegoInputParam() { i := &beegocontext.BeegoInput{}; sqlSink(i.Param("q")) } +func PositiveSourceBeegoInputParams() { i := &beegocontext.BeegoInput{}; sqlSink(i.Params()) } +func PositiveSourceBeegoInputURI() { i := &beegocontext.BeegoInput{}; sqlSink(i.URI()) } +func PositiveSourceBeegoInputURL() { i := &beegocontext.BeegoInput{}; sqlSink(i.URL()) } +func PositiveSourceBeegoInputRequestBody() { i := &beegocontext.BeegoInput{}; sqlSink(i.RequestBody) } +func UnsupportedPositiveSourceBeegoInputBind() { + i := &beegocontext.BeegoInput{} + var dst struct{ Q string } + sqlSinkMany(i.Bind(&dst)) +} +func PositiveSourceHeaderMethod() { + i := &beegocontext.BeegoInput{} + sqlSink(i.Header("q")) +} +func PositiveTrustSourceBeegoInputHeader() { + i := &beegocontext.BeegoInput{} + http.SetCookie(responseWriterWithString{}, cookieFrom(i.Header("q"))) +} + +func PositiveSQLDBQuery() { _, _ = db.Query("select " + envSource()) } +func PositiveSQLDBQueryContext() { _, _ = db.QueryContext(ctx, "select "+envSource()) } +func PositiveSQLDBQueryRow() { _ = db.QueryRow("select " + envSource()) } +func PositiveSQLDBQueryRowContext() { _ = db.QueryRowContext(ctx, "select "+envSource()) } +func PositiveSQLDBExec() { _, _ = db.Exec("select " + envSource()) } +func PositiveSQLDBExecContext() { _, _ = db.ExecContext(ctx, "select "+envSource()) } +func PositiveSQLDBPrepare() { _, _ = db.Prepare("select " + envSource()) } +func PositiveSQLDBPrepareContext() { _, _ = db.PrepareContext(ctx, "select "+envSource()) } + +func PositiveCmdExecCommand() { _ = exec.Command(envSource(), "arg") } +func PositiveCmdExecCommandContext() { _ = exec.CommandContext(ctx, envSource(), "arg") } +func PositiveCmdExecLookPath() { _, _ = exec.LookPath(envSource()) } +func PositiveCmdOSStartProcess() { + _, _ = os.StartProcess(envSource(), []string{envSource()}, &os.ProcAttr{}) +} +func PositiveCmdSyscallExec() { _ = syscall.Exec(envSource(), []string{envSource()}, os.Environ()) } +func PositiveCmdSyscallForkExec() { + _, _ = syscall.ForkExec(envSource(), []string{envSource()}, &syscall.ProcAttr{}) +} +func PositiveCmdSyscallStartProcess() { + _, _, _ = syscall.StartProcess(envSource(), []string{envSource()}, &syscall.ProcAttr{}) +} +func PositiveCmdCombinedOutput() { _, _ = exec.Command(envSource()).CombinedOutput() } +func PositiveCmdRun() { _ = exec.Command(envSource()).Run() } +func PositiveCmdOutput() { _, _ = exec.Command(envSource()).Output() } +func PositiveCmdStart() { _ = exec.Command(envSource()).Start() } + +func PositivePathOSOpen() { _, _ = os.Open(envSource()) } +func PositivePathOSOpenFile() { _, _ = os.OpenFile(envSource(), os.O_RDONLY, 0600) } +func PositivePathOSCreate() { _, _ = os.Create(envSource()) } +func PositivePathOSCreateTempDir() { _, _ = os.CreateTemp(envSource(), "p") } +func PositivePathOSCreateTempPattern() { _, _ = os.CreateTemp("", envSource()) } +func PositivePathOSMkdirTempDir() { _, _ = os.MkdirTemp(envSource(), "p") } +func PositivePathOSMkdirTempPattern() { _, _ = os.MkdirTemp("", envSource()) } +func PositivePathOSMkdir() { _ = os.Mkdir(envSource(), 0700) } +func PositivePathOSMkdirAll() { _ = os.MkdirAll(envSource(), 0700) } +func PositivePathOSRemove() { _ = os.Remove(envSource()) } +func PositivePathOSRemoveAll() { _ = os.RemoveAll(envSource()) } +func PositivePathOSReadFile() { _, _ = os.ReadFile(envSource()) } +func PositivePathOSWriteFile() { _ = os.WriteFile(envSource(), []byte("x"), 0600) } +func PositivePathOSReadDir() { _, _ = os.ReadDir(envSource()) } +func PositivePathOSStat() { _, _ = os.Stat(envSource()) } +func PositivePathOSLstat() { _, _ = os.Lstat(envSource()) } +func PositivePathOSTruncate() { _ = os.Truncate(envSource(), 1) } +func PositivePathOSChdir() { _ = os.Chdir(envSource()) } +func PositivePathOSChmod() { _ = os.Chmod(envSource(), 0600) } +func PositivePathOSChown() { _ = os.Chown(envSource(), 1, 1) } +func PositivePathOSLchown() { _ = os.Lchown(envSource(), 1, 1) } +func PositivePathOSChtimes() { _ = os.Chtimes(envSource(), time.Unix(1, 0), time.Unix(1, 0)) } +func PositivePathOSReadlink() { _, _ = os.Readlink(envSource()) } +func PositivePathOSRenameSource() { _ = os.Rename(envSource(), "safe") } +func PositivePathOSRenameTarget() { _ = os.Rename("safe", envSource()) } +func PositivePathOSLinkSource() { _ = os.Link(envSource(), "safe") } +func PositivePathOSLinkTarget() { _ = os.Link("safe", envSource()) } +func PositivePathOSSymlinkSource() { _ = os.Symlink(envSource(), "safe") } +func PositivePathOSSymlinkTarget() { _ = os.Symlink("safe", envSource()) } +func PositivePathOSDirFS() { _ = os.DirFS(envSource()) } +func PositivePathIoutilReadFile() { _, _ = ioutil.ReadFile(envSource()) } +func PositivePathIoutilWriteFile() { _ = ioutil.WriteFile(envSource(), []byte("x"), 0600) } +func PositivePathIoutilReadDir() { _, _ = ioutil.ReadDir(envSource()) } +func PositivePathIoutilTempFileDir() { _, _ = ioutil.TempFile(envSource(), "p") } +func PositivePathIoutilTempFilePattern() { _, _ = ioutil.TempFile("", envSource()) } +func PositivePathIoutilTempDirDir() { _, _ = ioutil.TempDir(envSource(), "p") } +func PositivePathIoutilTempDirPattern() { _, _ = ioutil.TempDir("", envSource()) } +func PositivePathHTTPServeFile() { + r := requestForSources() + http.ServeFile(responseWriterWithString{}, r, envSource()) +} + +func PositiveSSRFHTTPGet() { _, _ = http.Get(envSource()) } +func PositiveSSRFHTTPHead() { _, _ = http.Head(envSource()) } +func PositiveSSRFHTTPPost() { _, _ = http.Post(envSource(), "text/plain", strings.NewReader("x")) } +func PositiveSSRFHTTPPostForm() { _, _ = http.PostForm(envSource(), url.Values{"q": []string{"v"}}) } +func PositiveSSRFClientGet() { _, _ = client.Get(envSource()) } +func PositiveSSRFClientHead() { _, _ = client.Head(envSource()) } +func PositiveSSRFClientPost() { _, _ = client.Post(envSource(), "text/plain", strings.NewReader("x")) } +func PositiveSSRFClientPostForm() { + _, _ = client.PostForm(envSource(), url.Values{"q": []string{"v"}}) +} +func PositiveSSRFNewRequest() { _, _ = http.NewRequest(http.MethodGet, envSource(), nil) } +func PositiveSSRFNewRequestWithContext() { + _, _ = http.NewRequestWithContext(ctx, http.MethodGet, envSource(), nil) +} + +func PositiveSSTITemplateParse() { _, _ = template.New("x").Parse(envSource()) } +func PositiveSSTITemplateMustParse() { _ = template.Must(template.New("x").Parse(envSource())) } +func PositiveSSTITemplateParseFiles() { _, _ = template.New("x").ParseFiles(envSource()) } +func PositiveSSTITemplateParseGlob() { _, _ = template.New("x").ParseGlob(envSource()) } + +func PositiveXSSWrite() { _, _ = responseWriterWithString{}.Write([]byte(envSource())) } +func PositiveXSSWriteString() { _, _ = responseWriterWithString{}.WriteString(envSource()) } +func PositiveXSSBody() { beegoOutput{}.Body(envSource()) } +func PositiveXSSFprint() { _, _ = fmt.Fprint(responseWriterWithString{}, envSource()) } +func PositiveXSSFprintf() { _, _ = fmt.Fprintf(responseWriterWithString{}, "%s", envSource()) } +func PositiveXSSFprintln() { _, _ = fmt.Fprintln(responseWriterWithString{}, envSource()) } +func PositiveXSSIOWriteString() { _, _ = io.WriteString(responseWriterWithString{}, envSource()) } +func PositiveXSSJSONEncoderEncode() { + _ = json.NewEncoder(responseWriterWithString{}).Encode(envSource()) +} +func PositiveXSSServeJSON() { c := &jsonController{Data: envSource()}; c.ServeJSON() } +func PositiveXSSOutputJSON() { beegoOutput{}.JSON(envSource(), 200) } + +func NegativeXSSHTMLEscapeString() { + _, _ = responseWriterWithString{}.WriteString(template.HTMLEscapeString(envSource())) +} +func NegativeXSSJSEscapeString() { + _, _ = responseWriterWithString{}.WriteString(template.JSEscapeString(envSource())) +} +func UnsupportedNegativeXSSURLQueryEscaper() { + _, _ = responseWriterWithString{}.WriteString(template.URLQueryEscaper(envSource())) +} +func UnsupportedNegativeXSSHTMLEscaper() { + _, _ = responseWriterWithString{}.WriteString(template.HTMLEscaper(envSource())) +} +func NegativeXSSHTMLEscape() { + _, _ = responseWriterWithString{}.WriteString(html.EscapeString(envSource())) +} +func NegativeXSSURLQueryEscape() { + _, _ = responseWriterWithString{}.WriteString(url.QueryEscape(envSource())) +} +func NegativeXSSURLPathEscape() { + _, _ = responseWriterWithString{}.WriteString(url.PathEscape(envSource())) +} + +func PositiveTrustSetCookie() { http.SetCookie(responseWriterWithString{}, cookieFrom(envSource())) } +func PositiveTrustSetSession() { c := &web.Controller{}; _ = c.SetSession("q", envSource()) } + +func PositiveWeakCryptoDES() { _, _ = des.NewCipher([]byte("12345678")) } +func PositiveWeakCryptoTripleDES() { _, _ = des.NewTripleDESCipher([]byte("123456789012345678901234")) } +func PositiveWeakCryptoRC4() { _, _ = rc4.NewCipher([]byte("secret")) } +func PositiveWeakCryptoMD4() { _ = md4.New() } +func PositiveWeakCryptoCipherECBEncrypter() { _ = cipher.NewECBEncrypter("block") } +func PositiveWeakCryptoCipherECBDecrypter() { _ = cipher.NewECBDecrypter("block") } +func PositiveWeakCryptoECBEncrypter() { _ = NewECBEncrypter("block") } +func PositiveWeakCryptoECBDecrypter() { _ = NewECBDecrypter("block") } + +func NewECBEncrypter(args ...interface{}) interface{} { return args } +func NewECBDecrypter(args ...interface{}) interface{} { return args } + +func PositiveWeakHashMD5New() { _ = md5.New() } +func PositiveWeakHashMD5Sum() { _ = md5.Sum([]byte("x")) } +func PositiveWeakHashSHA1New() { _ = sha1.New() } +func PositiveWeakHashSHA1Sum() { _ = sha1.Sum([]byte("x")) } +func PositiveWeakHashMD4New() { _ = md4.New() } + +func PositiveWeakRandomInt() { _ = rand.Int() } +func PositiveWeakRandomIntn() { _ = rand.Intn(10) } +func PositiveWeakRandomInt31() { _ = rand.Int31() } +func PositiveWeakRandomInt31n() { _ = rand.Int31n(10) } +func PositiveWeakRandomInt63() { _ = rand.Int63() } +func PositiveWeakRandomInt63n() { _ = rand.Int63n(10) } +func PositiveWeakRandomFloat64() { _ = rand.Float64() } +func PositiveWeakRandomFloat32() { _ = rand.Float32() } +func PositiveWeakRandomNormFloat64() { _ = rand.NormFloat64() } +func PositiveWeakRandomExpFloat64() { _ = rand.ExpFloat64() } +func PositiveWeakRandomUint32() { _ = rand.Uint32() } +func PositiveWeakRandomUint64() { _ = rand.Uint64() } +func PositiveWeakRandomPerm() { _ = rand.Perm(10) } +func PositiveWeakRandomNew() { _ = rand.New(rand.NewSource(1)) } diff --git a/rules/test/go/security/sql-injection/sqlinj_01_env_basic/sample.go b/rules/test/go/security/sql-injection/sqlinj_01_env_basic/sample.go new file mode 100644 index 000000000..bbfceaa31 --- /dev/null +++ b/rules/test/go/security/sql-injection/sqlinj_01_env_basic/sample.go @@ -0,0 +1,20 @@ +package sqlinj_01_env_basic + +import ( + "os" +) + +// Sink_DbQuery represents a real *sql.DB.Query call site (SQL injection sink). +func Sink_DbQuery(query string) { _ = query } + +// Positive_basic: env var concatenated into SQL. +func Positive_basic() { + uid := os.Getenv("USER_ID") + Sink_DbQuery("SELECT * FROM users WHERE id='" + uid + "'") +} + +// Negative_const: env value discarded; constant goes in. +func Negative_const() { + _ = os.Getenv("USER_ID") + Sink_DbQuery("SELECT * FROM users WHERE id=1") +} diff --git a/rules/test/go/stubs/beego/go.mod b/rules/test/go/stubs/beego/go.mod new file mode 100644 index 000000000..17088c5fa --- /dev/null +++ b/rules/test/go/stubs/beego/go.mod @@ -0,0 +1,3 @@ +module github.com/beego/beego/v2 + +go 1.18 diff --git a/rules/test/go/stubs/beego/server/web/context/input.go b/rules/test/go/stubs/beego/server/web/context/input.go new file mode 100644 index 000000000..09ffbcba1 --- /dev/null +++ b/rules/test/go/stubs/beego/server/web/context/input.go @@ -0,0 +1,20 @@ +package context + +import ( + "net/http" + "net/url" +) + +type BeegoInput struct { + RequestBody []byte +} + +func (i *BeegoInput) Param(key string) string { return "" } +func (i *BeegoInput) Header(key string) string { return "" } +func (i *BeegoInput) Params() map[string]string { return nil } +func (i *BeegoInput) URI() string { return "" } +func (i *BeegoInput) URL() *url.URL { return nil } +func (i *BeegoInput) Bind(ptr interface{}) error { return nil } +func (i *BeegoInput) Cookie(key string) string { return "" } + +var _ = http.Header{} diff --git a/rules/test/go/stubs/beego/server/web/controller.go b/rules/test/go/stubs/beego/server/web/controller.go new file mode 100644 index 000000000..d72df14c0 --- /dev/null +++ b/rules/test/go/stubs/beego/server/web/controller.go @@ -0,0 +1,37 @@ +package web + +import ( + "mime/multipart" + + beegocontext "github.com/beego/beego/v2/server/web/context" +) + +type Controller struct { + Ctx *Context +} + +type Context struct { + Input *beegocontext.BeegoInput +} + +func (c *Controller) GetString(key string, def ...string) string { return "" } +func (c *Controller) GetStrings(key string, def ...string) []string { return nil } +func (c *Controller) GetInt(key string, def ...int) (int, error) { return 0, nil } +func (c *Controller) GetInt8(key string) (int8, error) { return 0, nil } +func (c *Controller) GetInt16(key string) (int16, error) { return 0, nil } +func (c *Controller) GetInt32(key string) (int32, error) { return 0, nil } +func (c *Controller) GetInt64(key string) (int64, error) { return 0, nil } +func (c *Controller) GetUint8(key string) (uint8, error) { return 0, nil } +func (c *Controller) GetUint16(key string) (uint16, error) { return 0, nil } +func (c *Controller) GetUint32(key string) (uint32, error) { return 0, nil } +func (c *Controller) GetUint64(key string) (uint64, error) { return 0, nil } +func (c *Controller) GetFloat(key string) (float64, error) { return 0, nil } +func (c *Controller) GetBool(key string) (bool, error) { return false, nil } +func (c *Controller) GetFile(key string) (multipart.File, *multipart.FileHeader, error) { + return nil, nil, nil +} +func (c *Controller) GetFiles(key string) ([]*multipart.FileHeader, error) { return nil, nil } +func (c *Controller) GetSession(key string) interface{} { return nil } +func (c *Controller) Input() map[string]string { return nil } +func (c *Controller) ParseForm(ptr interface{}) error { return nil } +func (c *Controller) SetSession(key string, value interface{}) error { return nil } diff --git a/rules/test/src/main/java/rules/RuleCoverageCheck.java b/rules/test/src/main/java/rules/RuleCoverageCheck.java index 076cf44f3..e8134cb93 100644 --- a/rules/test/src/main/java/rules/RuleCoverageCheck.java +++ b/rules/test/src/main/java/rules/RuleCoverageCheck.java @@ -23,8 +23,9 @@ /** * CI helper that checks that: * 1) every YAML in ../ruleset is valid; - * 2) every non-disabled and non-lib rule is covered by some @PositiveRuleSample test - * under src/main/java/security. + * 2) every non-disabled and non-lib rule is covered by either a Java + * @PositiveRuleSample test under src/main/java/security or a Go rule-test.yaml + * entry under go/. * * Fails with non-zero exit code and prints all problems. */ @@ -132,6 +133,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { private static Set