Go code auditing
TL;DR: Go’s static typing and
errcheckdiscipline kill whole bug classes the older languages bleed on. What’s left: SSRF viahttp.Get, command injection viaexec.Commandwith concat, SQLi viafmt.Sprintfqueries, unsafe deserialization ingob/encoding/asn1/third-party packages, path traversal viafilepath.Join, goroutine race conditions on shared state, and dangerous use ofreflect/unsafe. See dangerous-go-sinks for the catalogue.
What it is
Go’s “boring” reputation makes auditors lazy — most bugs hide in the os/exec, text/template, and ORM-helper layers. The typical Go service is a net/http mux or chi/gorilla/echo router → handlers → sqlx/gorm/pgx → response. Concurrency adds a TOCTOU surface that single-threaded runtimes don’t have.
Preconditions / where it applies
- Source (
.go,go.mod) - Go version (generics from 1.18; key crypto fixes per release;
errors.Ispatterns) - Framework choice — net/http, gin, echo, chi, fiber, gqlgen
Technique
- Map entry points.
net/http:http.HandleFunc("/path", ...),mux.Handle,http.Handlerinterfaces.chi/gorilla:r.Get("/path", handler).gin/echo/fiber:r.GET/POST/....- gRPC: handlers generated from
.proto. - Middleware:
func(http.Handler) http.Handlerdecorators.
- Trace sources.
r.URL.Query(),r.Form(afterr.ParseForm),r.PostForm,r.Body(raw),r.Cookie,r.Header,chi.URLParam,mux.Vars. JSON binding viajson.NewDecoder(r.Body).Decode(&v)— check struct tags + unexported fields. - Sink catalogue — see dangerous-go-sinks:
1 2 3 4 5 6 7 8 9 10
rg -n 'exec\.Command\([^,]*\+' . # concat in cmd rg -n 'exec\.Command\("(sh|bash|cmd)"' . # shell args rg -n 'fmt\.Sprintf\(.*SELECT|fmt\.Sprintf\(.*INSERT|fmt\.Sprintf\(.*UPDATE' . # SQLi via fmt rg -n 'db\.(Query|Exec|QueryRow)\(\s*fmt\.Sprintf' . # same rg -n 'gorm\.\(Raw\|Exec\)\(\s*fmt\.Sprintf' . # gorm SQLi rg -n 'http\.Get\(|http\.Post\(|http\.NewRequest\("[A-Z]+",\s*r\.' . # SSRF rg -n 'text/template.*Parse|html/template.*Parse' . # template injection if user-controlled template rg -n 'gob\.NewDecoder|encoding/gob' . # gob deserialization rg -n 'filepath\.Join\(.*r\.URL|os\.Open\(\s*r\.URL' . # path traversal rg -n 'unsafe\.Pointer|reflect\.NewAt' . # unsafe — review carefully
- Command injection.
exec.Command("/bin/sh", "-c", "ls " + userInput)is RCE.exec.Command("ls", userInput)is arg-safe but flag injection persists (userInput = "-l /etc/passwd"). Reject leading-or use--separator. - SQL injection.
database/sqlwith?/$1placeholders is safe.fmt.Sprintf("SELECT * FROM users WHERE name='%s'", x)is not.gorm.Raw(fmt.Sprintf(...))is SQLi.sqlx.MustExec(...)withSprintfis SQLi. - SSRF.
http.Get(userURL)with no allowlist. Stdlib redirect-follow default is 10 hops — DNS rebinding possible on each. Wraphttp.Client.TransportwithDialContextthat checks resolved IP against denylist (RFC1918, link-local, metadata IPs). - Path traversal.
filepath.Join(root, userPath)does NOT prevent... Usefilepath.Clean+ check that the cleaned result hasrootas prefix and no..after.1 2 3 4
clean := filepath.Clean(filepath.Join(root, user)) if !strings.HasPrefix(clean, root+string(os.PathSeparator)) { return errInvalid }
- Template injection.
text/templatedoes not auto-escape (use only for non-HTML).html/templateescapes context-aware. Both expose.Funcsallowing custom functions — if a template loaded from disk is user-controlled, RCE. - Deserialization.
gob,encoding/asn1,xml.UnmarshalwithDisallowUnknownFields=false, third-partymsgpackwith type discrimination,protobufAnytypes — each is a gadget surface.json.Unmarshalis generally safe butinterface{}targets lose type discipline. - Race conditions. Run
go test -race. Shared maps withoutsync.Mutex,sync.Map, or channels.time.Now()+ uniqueness check is a classic TOCTOU. Goroutine leaks from missingselect{}on cancelable contexts. - Crypto and tokens.
math/randfor tokens — flag.crypto/randonly for secrets.crypto/subtle.ConstantTimeComparefor HMAC checks, not==.golang.org/x/crypto/bcryptcost ≥10. JWT libs vary —github.com/golang-jwt/jwt/v5requires explicit method. - Mass assignment via JSON.
json.NewDecoder(r.Body).Decode(&user)populates every exported field ofUser. Use a DTO struct for input and copy to model. SetDecoder.DisallowUnknownFields()for stricter rejection. - Race in middleware. Shared
*http.Requestacross goroutines —r.Context()cancels on response write; reads after that return errors silently.
Detection and defence
staticcheck,gosec,govulncheckin CI — first two are AST/SSA based, last is dep-CVE.goplsandgo vetfor obvious bugs;-racefor data races.- Semgrep
go.lang.securityruleset. - Force prepared queries via a Rubocop-like ban on
fmt.Sprintfin query paths. - Use
errors.Is/errors.Asfor unwrapping; rawerr == sql.ErrNoRowsbreaks on wrapping.
References
- Go security best practices
- gosec — primary SAST
- govulncheck — CVE deps
- HackTricks — Go (general)
- See also: dangerous-go-sinks