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.
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.
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
}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@latestThe 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 ./myprojectloads the local module using pattern./...(recursive) by default.load https://github.com/user/repoclones (shallow) and loads only the root package (.) by default, not the whole repo.load https://github.com/user/repo/cmd/toolclones 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 expressionsExample 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$1check <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).QueryOther commands: cg (show callgraph), nodes (list nodes), pkgs (list packages), root (show root), clear, exit
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 ./...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.
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.
The sqli analyzer finds potential SQL injections.
Supported SQL packages include:
- the standard library
database/sqlpackage (Query,QueryRow,Exec, andPreparevariants onDB,Tx, andConn) github.com/jinzhu/gorm(GORM v1)gorm.io/gorm(GORM v2)github.com/jmoiron/sqlxgithub.com/go-gorm/gorm(GORM v2 alt)xorm.io/xormandgithub.com/go-xorm/xormgithub.com/go-pg/pggithub.com/rqlite/gorqlitegithub.com/raindog308/gorqlitegithub.com/Masterminds/squirreland variantsgithub.com/jackc/pgx/v5andgithub.com/jackc/pgx/v5/pgxpool(Conn,Tx, andPoolquery 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,Rawon theDBinterface)- 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 injectionThe 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 injectionThe 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 injectionThe 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 XSSThe 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 traversalThe 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 forgerytaint 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.
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.
