Skip to content

picatz/taint

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

99 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

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.

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 ./...

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
  • 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, and go.uber.org/zap. 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

About

🚰 Static taint analysis for Go programs.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors

Languages