Skip to content
picatzPublic

About

🚰 Static taint analysis for Go programs.

Resources

Stars

85 stars

Watchers

2 watching

Forks

Latest commit

Β 

History

203 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

taint

Implements static taint analysis for Go programs.

Taint analysis is a technique for identifying the flow of sensitive data through a program. It can be used to identify potential security vulnerabilities, such as SQL injection or cross-site scripting (XSS) attacks, by understanding how this data is used and transformed as it flows through the code.

A "source" is a point in the program where sensitive data originates, typically from user input, such as data entered into a form on a web page, or data loaded from an external source. A "sink" is a point in the program where sensitive data is used or transmitted to exploit the program.

Go support

Building from source requires Go 1.26 or newer. The preferred toolchain is Go 1.27.1, and CI builds and runs the test suite on the latest Go 1.26 and 1.27 patch releases. Use a current patch release for security fixes. The minimum version increased from Go 1.25 with the go-git v5.19.3 security update.

Go 1.27 regression tests exercise generic methods, promoted fields in struct literals, and expanded generic function inference in the programs being analyzed, with both tainted and clean inputs.

Example

This code generates a function call graph rooted at a program's main function and then runs taint analysis on it. If the program uses database/sql, the taint analysis will determine if the program is vulnerable to SQL injection such that any of the given sources reach the given sinks.

cg, err := callgraphutil.NewGraph(mainFn, buildSSA.SrcFuncs...)
if err != nil {
	return err
}

sources := taint.NewSources(
	"*net/http.Request",
	"google.golang.org/protobuf/proto.Message", // gRPC request types
)

sinks := taint.NewSinks(
	"(*database/sql.DB).Query",
	"(*database/sql.DB).QueryContext",
	"(*database/sql.DB).QueryRow",
	"(*database/sql.DB).QueryRowContext",
	"(*database/sql.Tx).Query",
	"(*database/sql.Tx).QueryContext",
	"(*database/sql.Tx).QueryRow",
	"(*database/sql.Tx).QueryRowContext",
)

results := taint.Check(cg, sources, sinks)

for _, result := range results {
	// We found a query edge that is tainted by user input, is it
	// doing this safely? We expect this to be safely done by
	// providing a prepared statement as a constant in the query
	// (first argument after context).
	queryEdge := result.Path[len(result.Path)-1]

	// Get the query arguments, skipping the first element, pointer to the DB.
	queryArgs := queryEdge.Site.Common().Args[1:]

	// Skip the context argument, if using a *Context query variant.
	if strings.HasSuffix(queryEdge.Site.Value().Call.Value.String(), "Context") {
		queryArgs = queryArgs[1:]
	}

	// Get the query function parameter.
	query := queryArgs[0]

	// Ensure it is a constant (prepared statement), otherwise report
	// potential SQL injection.
	if _, isConst := query.(*ssa.Const); !isConst {
		pass.Reportf(result.SinkValue.Pos(), "potential sql injection")
	}
}

For explainability, use CheckDetailed instead. It preserves the Result shape and adds ordered evidence for source matches, propagation, parameter mapping, sanitizer decisions, sink matches, and conservative unknowns.

diagnostics := taint.CheckDetailed(cg, sources, sinks, taint.WithSanitizers("html.EscapeString"))
for _, diagnostic := range diagnostics {
	result := diagnostic.Result
	for _, evidence := range diagnostic.Evidence {
		fmt.Printf("%s: %s\n", evidence.Kind, evidence.Message)
	}
	_ = result
}

taint

The taint CLI is an interactive tool to find potential security vulnerabilities. It can be used to find potential SQL injections, log injections, command injections, and cross-site scripting (XSS) vulnerabilities, among other types of vulnerabilities.

$ go install github.com/picatz/taint/cmd/taint@latest

demo

Interactive Commands

The taint tool provides several interactive commands for exploring callgraphs and analyzing code:

load <target> [pattern] [--full] - Load a Go program or package for analysis

Targets can be local directories or GitHub URLs. When providing a GitHub URL you may optionally append a subdirectory or specific file path to restrict loading. This helps when large repositories contain many commands / main packages and you only want to look at a single entry point.

  • load ./myproject loads the local module using pattern ./... (recursive) by default.
  • load https://github.com/user/repo clones (shallow) and loads only the root package (.) by default, not the whole repo.
  • load https://github.com/user/repo/cmd/tool clones and loads ./cmd/tool (pattern defaults to . under that path).

callpath <function> - Find call paths to functions with flexible matching strategies:

> callpath fmt.Printf                    # Exact match
> callpath fuzzy:Printf                  # Substring/fuzzy matching  
> callpath glob:fmt.*                    # Shell-style glob patterns
> callpath regex:.*\.(Exec|Query)$       # Regular expressions

Example output:

> load xss
βœ“ loaded 1 packages, creating 1 SSA packages
βœ“ created multi-root callgraph with 3 potential roots

> callpath fuzzy:Check
βœ“ found 1 path(s) using fuzzy matching for: fuzzy:Check
1: n0:github.com/picatz/taint/xss.run β†’ n8:github.com/picatz/taint.Check

> callpath fuzzy:run  
βœ“ found 2 path(s) using fuzzy matching for: fuzzy:run
1: n0:github.com/picatz/taint/xss.run (root node)
2: n0:github.com/picatz/taint/xss.run β†’ n9:github.com/picatz/taint.WalkSSA β†’ n10:github.com/picatz/taint/xss.run$1

check <source> <sink> - Perform taint analysis between source and sink functions

> load ./cmd/taint/example
βœ“ loaded 1 packages, creating 1 SSA packages
  building SSA package 0: main
βœ“ found main function, using as callgraph root
βœ“ loaded 1 packages
> cg
n0:github.com/picatz/taint/cmd/taint/example.main
   β†’ n4:database/sql.Open
   β†’ n5:net/http.NewServeMux
   β†’ n6:(*net/http.ServeMux).HandleFunc
   β†’ n8:net/http.ListenAndServe

n1:github.com/picatz/taint/cmd/taint/example.handle
   β†’ n3:(*database/sql.DB).Query

n2:github.com/picatz/taint/cmd/taint/example.business
   β†’ n1:github.com/picatz/taint/cmd/taint/example.handle

n3:(*database/sql.DB).Query

n4:database/sql.Open

n5:net/http.NewServeMux

n6:(*net/http.ServeMux).HandleFunc
   β†’ n7:github.com/picatz/taint/cmd/taint/example.main$1

n7:github.com/picatz/taint/cmd/taint/example.main$1
   β†’ n9:(*net/url.URL).Query
   β†’ n2:github.com/picatz/taint/cmd/taint/example.business

n8:net/http.ListenAndServe

n9:(*net/url.URL).Query

> check *net/http.Request (*database/sql.DB).Query
n0:github.com/picatz/taint/cmd/taint/example.main β†’ n6:(*net/http.ServeMux).HandleFunc β†’ n7:github.com/picatz/taint/cmd/taint/example.main$1 β†’ n2:github.com/picatz/taint/cmd/taint/example.business β†’ n1:github.com/picatz/taint/cmd/taint/example.handle β†’ n3:(*database/sql.DB).Query
> check (*net/url.URL).Query (*database/sql.DB).Query
n0:github.com/picatz/taint/cmd/taint/example.main β†’ n6:(*net/http.ServeMux).HandleFunc β†’ n7:github.com/picatz/taint/cmd/taint/example.main$1 β†’ n2:github.com/picatz/taint/cmd/taint/example.business β†’ n1:github.com/picatz/taint/cmd/taint/example.handle β†’ n3:(*database/sql.DB).Query

Other commands: cg (show callgraph), nodes (list nodes), pkgs (list packages), root (show root), clear, exit

Analyzer CLI Options

The sqli, logi, cmdi, xss, ptrv, and ssrf analyzer CLIs use the built-in taint callgraph by default. Pass -callgraph=vta to compare results with the alternate CHA+VTA builder.

They also support SARIF output for code scanning integrations:

$ sqli -sarif ./... > sqli.sarif
$ logi -sarif-output logi.sarif ./...
$ cmdi -sarif-output cmdi.sarif ./...
$ xss -sarif ./...
$ ptrv -sarif-output ptrv.sarif ./...
$ ssrf -sarif-output ssrf.sarif ./...

vuln

The vuln command scans a module for known vulnerabilities from the Go vulnerability database and ranks each finding by how strongly the code is exposed, from merely depending on a vulnerable module up to attacker-controlled data reaching the vulnerable symbol. It is this project's taint-aware answer to govulncheck: the same data and reachability, plus a taint tier with a data-flow trace.

$ go run github.com/picatz/taint/cmd/vuln@latest ./...

Findings are ranked by tier (taint > symbol > package > module), it emits text, JSON, or SARIF, and it exits 3 when findings are reported so CI can gate on it. See docs/vulncheck.md.

Custom models

Beyond the built-in coverage below, you can teach any detector about your own frameworks and helpers with models: data-driven sources, sinks, sanitizers, and flow summaries in YAML. Pass -models path/ to a detector CLI, or use taint.WithModels from the library. Whole-program scans also accept models: taint scan -analyzers=sqli -models models/ ./.... With -models, scan requires exactly one selected analyzer: model kind labels are informational, and findings use that analyzer's identity. Relative model paths resolve under -C when supplied, otherwise under the current directory. Models augment the built-in rules without changing the selected occurrence scope or same-module body limits. See docs/models.md.

sqli

The sqli analyzer finds potential SQL injections.

Supported SQL packages include:

  • the standard library database/sql package (Query, QueryRow, Exec, and Prepare variants on DB, Tx, and Conn)
  • github.com/jinzhu/gorm (GORM v1)
  • gorm.io/gorm (GORM v2)
  • github.com/jmoiron/sqlx
  • github.com/go-gorm/gorm (GORM v2 alt)
  • xorm.io/xorm and github.com/go-xorm/xorm
  • github.com/go-pg/pg
  • github.com/rqlite/gorqlite
  • github.com/raindog308/gorqlite
  • github.com/Masterminds/squirrel and variants
  • github.com/jackc/pgx/v5 and github.com/jackc/pgx/v5/pgxpool (Conn, Tx, and Pool query methods; only the SQL-text argument is treated as the injection channel, so bound parameters do not flag)
  • github.com/beego/beego/v2/client/orm (Raw, RawWithCtx)
  • github.com/gogf/gf/v2/database/gdb (Query, Exec, GetAll, GetOne, Raw on the DB interface)
  • database drivers like github.com/mattn/go-sqlite3
$ go install github.com/picatz/taint/cmd/sqli@latest
$ cd sql/injection/testdata/src/example
$ cat main.go
package main

import (
        "database/sql"
        "net/http"
)

func business(db *sql.DB, q string) {
        db.Query(q) // potential sql injection
}

func run() {
        db, _ := sql.Open("sqlite3", ":memory:")

        mux := http.NewServeMux()

        mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                business(db, r.URL.Query().Get("sql-query"))
        })

        http.ListenAndServe(":8080", mux)
}

func main() {
        run()
}
$ sqli main.go
./sql/injection/testdata/src/example/main.go:9:10: potential sql injection

logi

The logi analyzer finds potential log injections. It understands common logging packages, including log, log/slog, github.com/golang/glog, github.com/hashicorp/go-hclog, github.com/sirupsen/logrus, go.uber.org/zap, github.com/rs/zerolog, github.com/go-logr/logr, k8s.io/klog/v2, and github.com/go-kit/log. For structured logging, field constructors such as slog.String, slog.Any, zap.String, zap.Any, zap.ByteString, and zap.Error preserve taint into the logged field.

$ go install github.com/picatz/taint/cmd/logi@latest
$ cd log/injection/testdata/src/a
$ cat main.go
package main

import (
        "log"
        "net/http"
)

func main() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                log.Println(r.URL.Query().Get("input"))
        })

        http.ListenAndServe(":8080", nil)
}
$ logi main.go
./log/injection/testdata/src/a/main.go:10:14: potential log injection

cmdi

The cmdi analyzer finds potential command injection issues. It reports tainted executable names passed to os/exec.Command or os/exec.CommandContext, and tainted shell command strings in calls such as exec.Command("sh", "-c", input). It does not flag every tainted ordinary process argument in the v1 rule.

$ go install github.com/picatz/taint/cmd/cmdi@latest
$ cd command/injection/testdata/src/direct
$ cat main.go
package main

import (
        "net/http"
        "os/exec"
)

func run(r *http.Request) {
        exec.Command(r.FormValue("cmd")) // want "potential command injection"
}

func main() {
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
                run(r)
        })
}
$ cmdi main.go
./command/injection/testdata/src/direct/main.go:9:14: potential command injection

xss

The xss analyzer finds potential cross-site scripting (XSS) vulnerabilities. It reports tainted writes through http.ResponseWriter.Write, io.WriteString, io.Copy, net/http.Error, and fmt.Fprint/fmt.Fprintf/fmt.Fprintln when the destination flows from an HTTP response writer.

$ go install github.com/picatz/taint/cmd/xss@latest
$ cd xss/testdata/src/a
$ cat main.go
package main

import (
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(r.URL.Query().Get("input"))) // want "potential XSS"
	})

	http.ListenAndServe(":8080", nil)
}
$ xss main.go
./xss/testdata/src/a/main.go:9:10: potential XSS

ptrv

The ptrv analyzer finds potential path traversal vulnerabilities (CWE-22). It reports tainted file paths reaching os.Open, os.OpenFile, os.Create, os.ReadFile, os.WriteFile, os.Remove, os.RemoveAll, os.Mkdir, os.MkdirAll, io/ioutil.ReadFile, io/ioutil.WriteFile, io/ioutil.ReadDir, and the name argument of net/http.ServeFile.

Path-manipulation helpers (path.Join, path/filepath.Join, path/filepath.Clean, path/filepath.Abs, and the rest of the path / path/filepath family) propagate taint because lexical cleaning alone does not prevent directory escape; combine with a prefix-check sanitizer if you rely on confinement.

$ go install github.com/picatz/taint/cmd/ptrv@latest
$ cd command/pathtraversal/testdata/src/direct
$ cat main.go
package main

import (
	"net/http"
	"os"
)

func run(r *http.Request) {
	os.Open(r.FormValue("file")) // want "potential path traversal"
}

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		run(r)
	})
}
$ ptrv main.go
./command/pathtraversal/testdata/src/direct/main.go:9:9: potential path traversal

ssrf

The ssrf analyzer finds potential server-side request forgery vulnerabilities (CWE-918). It reports tainted URL or address values reaching outbound network APIs such as net/http.Get, net/http.Post, net/http.PostForm, net/http.Head, net/http.NewRequest, net/http.NewRequestWithContext, the *net/http.Client request methods (Do, Get, Post, PostForm, Head), and net.Dial / net.DialTimeout.

$ go install github.com/picatz/taint/cmd/ssrf@latest
$ cd network/ssrf/testdata/src/direct
$ cat main.go
package main

import (
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		_, _ = http.Get(r.URL.Query().Get("url")) // want "potential server-side request forgery"
	})
}
$ ssrf main.go
./network/ssrf/testdata/src/direct/main.go:9:18: potential server-side request forgery

Whole-program coverage

taint scan -coverage [packages] explains which imported same-module package bodies are omitted, on stderr, without changing finding output or scan scope. See body coverage for dependency and workspace limits.

Selected-package analysis

taint scan -scope=selected [packages] opts into exact selected-package roots and selected source/sink occurrences while retaining selected-package-only SSA bodies. -scope=legacy is the unchanged default. External modeled APIs called from selected code remain eligible; dependencies do not acquire source bodies. See analysis scope for initializer, method, mixed command/library, and propagation semantics. Add -coverage for stderr-only profile and body-coverage diagnostics.

For bounded dependency bodies without widening roots or source/sink ownership, use -scope=selected -bodies=same-module -max-body-packages=N -max-body-syntax-bytes=N. Both additional-input budgets must be explicitly positive. Exceeding either fails the scan before SSA construction, with no findings output. These bound parsed inputs, not memory or execution time; see body coverage for exact semantics and limits.

About

🚰 Static taint analysis for Go programs.

Resources

Stars

85 stars

Watchers

2 watching

Forks

Releases

Used by

Contributors

Languages