Go removes whole classes of memory corruption bugs, but Go services still ship with injection flaws, missing timeouts, path traversal, data races and vulnerable dependencies. Most Go security incidents come from the gap between what the language guarantees and what developers assume it guarantees: text/template does not escape HTML, http.DefaultClient never times out, and unsafe or cgo switch memory safety off. This guide covers the pitfalls that show up most often in Go code review and the standard library patterns that avoid them, plus the toolchain controls (govulncheck, the race detector, native fuzzing, module checksums) that make those patterns stick. It is written for Go developers, reviewers and AppSec engineers who already know the general principles in the Secure Coding Guide and want the Go-specific detail.
Key takeaways
- Memory safety is not application safety. Go's bounds checks and garbage collector stop buffer overflows and use-after-free in pure Go code, but injection, authorisation, resource exhaustion and logic flaws are unchanged, and
unsafeand cgo opt out of the guarantees. - Pick the safe package, not the similar one.
html/templateovertext/templatefor HTML,crypto/randovermath/rand,os.Rootoverfilepath.Joinwith user input, andexec.Commandwith separate arguments oversh -c. - Every network boundary needs explicit limits. Set server timeouts, a request body cap with
http.MaxBytesReader, and a clientTimeoutor context deadline. The zero values mean "no limit". - Concurrency bugs are security bugs. Data races on maps, sessions and authorisation state corrupt data and can crash the process. Run tests with
-raceand treatgo vetfindings as build failures. - Use the toolchain's supply chain controls. Commit
go.modandgo.sum, keep the checksum database on, rungovulncheck ./...in CI and stay on a supported Go point release, because security fixes ship in minor releases.
What Go protects you from, and what it does not
Go is memory safe in the ordinary sense: slices and arrays are bounds checked at run time, memory is garbage collected and pointer arithmetic is not available outside the unsafe package. That removes the out-of-bounds writes and use-after-free bugs that dominate C and C++ advisories (see the Memory-Safe Languages Guide). It does not remove the rest of the vulnerability catalogue.
| Risk area | What Go gives you | What is still on you |
|---|---|---|
| Buffer overflow, use-after-free | Bounds checks, garbage collection | Code using unsafe.Pointer, unsafe.Slice or cgo, and C libraries linked through cgo |
| Integer overflow | Defined wrap-around behaviour, no undefined behaviour | Wrapped lengths and offsets used for allocation or slicing, conversions between int64 and int32 |
| Injection (SQL, OS command, template) | Safe APIs exist in the standard library | Using them consistently; string concatenation still compiles |
| Concurrency | Goroutines and channels, race detector | Data races on shared maps and structs; concurrent map writes cause a fatal runtime error |
| Denial of service | Header size default (1 MB), MaxBytesReader | Server and client timeouts, body caps, goroutine and decompression limits |
| Panics | net/http recovers panics per request | Panics in goroutines you start yourself crash the whole process |
| Dependencies | go.sum, checksum database, no install scripts | Choosing, reviewing and patching modules; init functions still run at start-up |
The practical rule for reviewers: any file importing unsafe, C (cgo) or reflect to write values deserves a closer look, because those are the places where Go's usual guarantees stop.
Injection: SQL, OS commands and templates
SQL
- Use placeholders: pass values as arguments to
db.QueryContext(ctx, "SELECT ... WHERE id = $1", id)(PostgreSQL drivers) or?(MySQL and SQLite drivers). Never build the query withfmt.Sprintfor+. - Identifiers cannot be parameterised: table names, column names and
ORDER BYdirections must come from an allow-list map in code, not from the request. - Query builders and ORMs: check that raw-SQL escape hatches (methods taking a format string) are not fed request data. This is where most Go SQL injection findings sit. See the SQL Injection Prevention Guide.
OS commands
- No shell by default:
os/execintentionally does not invoke the system shell, soexec.CommandContext(ctx, "convert", in, out)passes each argument as a separate string with no globbing, pipes or redirection. The risk returns the moment code callsexec.Command("sh", "-c", userString). - Argument injection: a value starting with
-can still be read as an option by the target program. Put a--separator before user-supplied file names where the program supports it, and validate against an allow-list. - Binary lookup: since Go 1.19,
exec.LookPathno longer resolves a program from the current directory and returns an error matchingexec.ErrDotinstead. Do not re-enable the old behaviour withGODEBUG=execerrdot=0; use absolute paths for privileged helpers. See the Command Injection Guide.
Templates
- html/template for anything a browser renders: it escapes contextually, so the same value is encoded differently in HTML text, attributes, URLs, CSS and JavaScript. A value that fails the URL safety check is replaced with
#ZgotmplZ, which is a signal to investigate, not to suppress. - text/template has the same API and no escaping: a wrong import is a full XSS bug that compiles cleanly. Lint for
text/templatein any package that writes HTTP responses. - Typed escape hatches:
template.HTML,template.JS,template.URL,template.HTMLAttrandtemplate.CSSmark content as trusted and skip escaping. Converting request data or third-party content into these types reintroduces XSS. Untrusted JSON should be unmarshalled and passed as a value, not wrapped intemplate.JS. - Template source is trusted: the package assumes template authors are trusted and only the data is not. Never parse a template from user input. See the Server-Side Template Injection (SSTI) Guide and the Cross-Site Scripting (XSS) Prevention Guide.
HTTP servers and clients: limits, timeouts and CSRF
The convenience functions in net/http are tuned for examples, not production. A http.Server with zero-value timeouts keeps slow connections open indefinitely, and http.DefaultClient is a plain &Client{} whose zero Timeout means no timeout at all.
| Setting | Default | Safer pattern | Threat addressed |
|---|---|---|---|
Server.ReadHeaderTimeout | Zero (no limit) | A few seconds | Slow-header (Slowloris style) connection exhaustion |
Server.ReadTimeout / WriteTimeout | Zero | Set per service; longer only for streaming handlers | Slow bodies and stuck responses holding goroutines |
Server.IdleTimeout | Falls back to ReadTimeout | Explicit value | Idle keep-alive connection build-up |
Server.MaxHeaderBytes | DefaultMaxHeaderBytes (1 MB) | Lower if the API never needs large headers | Memory pressure from oversized headers |
| Request body size | Unlimited | Wrap with http.MaxBytesReader(w, r.Body, n); handle *http.MaxBytesError | Memory exhaustion from large JSON or uploads |
Client.Timeout | Zero (no timeout) | Dedicated client with a timeout, plus context.WithTimeout per call | Goroutine leaks when a dependency hangs |
| Cross-origin POSTs | Not checked | http.CrossOriginProtection (Go 1.25) or a token scheme | CSRF on cookie-authenticated endpoints |
- CSRF: Go 1.25 added
http.CrossOriginProtection, which rejects non-safe cross-origin browser requests using theSec-Fetch-SiteFetch metadata header, with no tokens or cookies needed. It supports trusted-origin and path-based bypasses; keep those lists short and reviewed. - Decompression and parsing: cap the size of anything you decompress or decode (
io.LimitReaderaround gzip readers), and cap multipart memory with the argument toParseMultipartForm. - JSON strictness: use
json.DecoderwithDisallowUnknownFields()on security-relevant inputs, and decode into request-specific structs rather than the domain model, so a client cannot set fields such asIsAdmin(mass assignment). See the Secure API Design Guide. - Outbound requests to user-supplied URLs: validate the resolved IP, not the host name, inside a custom
net.DialerControlfunction, so redirects and DNS rebinding cannot reach metadata endpoints or internal ranges. See the SSRF Prevention Guide.
Files, paths and archives
filepath.Join(base, userInput) cleans the path but does not keep it inside base: a name containing ../ or an absolute path walks out of the directory, and a symbolic link inside the directory can point anywhere. Go 1.24 added a traversal-resistant API for exactly this case.
- os.Root: open the directory once with
os.OpenRoot(dir)and use itsOpen,Create,Stat,MkdirandRemovemethods with relative names, or useos.OpenInRoot(dir, name)for a single file. Names that escape through..or through symbolic links pointing outside the root are rejected, and on Windows reserved device names such asNULandCOM1are blocked. - Know the limits: on Unix, traversal of mount points (for example Linux bind mounts) is not restricted; creating symbolic links and renaming are not yet supported; on
GOOS=jsthe symlink checks are open to time-of-check to time-of-use races, and on WASI protection depends on the runtime. - Archives: when extracting zip or tar files, ignore the stored path for the destination where you can, otherwise extract through an
os.Root, and cap the total uncompressed size and file count (zip slip and decompression bombs). - Permissions: create sensitive files with
0600and directories with0700, and useos.CreateTemprather than predictable names in shared temporary directories.
See the Path Traversal Guide for the attack patterns and server-side controls that apply across languages.
Cryptography, randomness and secrets
| Task | Use | Avoid |
|---|---|---|
| Tokens, session IDs, reset codes, keys | crypto/rand (rand.Read, rand.Text on recent releases) | math/rand and math/rand/v2: their outputs may be predictable however they are seeded |
| Comparing MACs, tokens, API keys | crypto/subtle.ConstantTimeCompare or hmac.Equal | == or bytes.Equal on secrets |
| Password storage | golang.org/x/crypto/argon2 or bcrypt | Plain SHA-256 or MD5 of the password |
| Symmetric encryption | AES-GCM via crypto/cipher with a fresh random nonce per message, or x/crypto/chacha20poly1305 | Unauthenticated modes, reused nonces |
| TLS clients | Default crypto/tls configuration, MinVersion of TLS 1.2 or higher | InsecureSkipVerify: true outside tests |
| FIPS 140-3 environments | Native Go Cryptographic Module (Go 1.24+), selected at build time with GOFIPS140 | GODEBUG=fips140=only in production; the Go team describes it as a best-effort testing mode |
- TLS defaults keep improving: Go 1.25 disallows SHA-1 signature algorithms in TLS 1.2 handshakes (re-enable only temporarily with
GODEBUG=tlssha1=1). Staying current is often a better hardening step than hand-written cipher suite lists. See the How TLS Works Guide. - FIPS: module v1.0.0, frozen from Go 1.24, holds CMVP certificate #5247.
GOFIPS140=v1.0.0pins it;GODEBUG=fips140=onenables FIPS mode at run time and cannot be changed after start-up. - Secrets in binaries: strings compiled into a Go binary are trivially recoverable, and
-ldflags "-X ..."injection is not a secrets mechanism. Load API keys and database credentials at run time from a secrets manager or workload identity, and never log them (watch%+von structs holding credentials). See the Secrets Management Guide.
Concurrency, panics and error handling
- Data races: unsynchronised access to shared maps, slices and structs can corrupt state, and concurrent map writes terminate the process. Guard shared state with
sync.Mutex,sync.RWMutexor channels, and do not cache per-user authorisation decisions in global maps without locking. See the Race Condition Vulnerabilities Guide. - Race detector:
go test -race ./...reports races it observes at run time, so it only finds races on code paths the tests execute. Pair it with tests that exercise concurrent handlers. - Time-of-check to time-of-use: checking a file with
os.Statthen opening it, or checking a balance then debiting it in two queries, is a race regardless of language. Use atomic operations: open thenStatthe handle, and database transactions with row locks or conditional updates. - Goroutine panics:
net/httprecovers a panicking handler, but a panic in a goroutine you start yourself takes down the whole service. Recover in long-lived workers, and bound goroutine creation per request (worker pools,errgroupwithSetLimit). - Error checking: ignored errors from
Close,Writeor authorisation lookups turn failures into silent success. Go 1.25 fixed a compiler bug that could delay nil pointer checks, so code that uses a result before checkingerrnow panics as the specification requires; treat that as a bug to fix, not a regression. - Error messages: return generic errors to clients and log details server-side; wrapped errors (
%w) often contain SQL, file paths or upstream responses. See the Secure Error Handling and Logging Guide.
Dependencies and the Go module supply chain
Go modules have stronger defaults than most ecosystems: go.mod fully determines every dependency version, builds fail if it is incomplete (since Go 1.16), go.sum records a cryptographic hash of each module, and the checksum database is a global append-only log that lets every user verify they received the same module contents. Fetching and building do not run package code, so there are no install hooks, although every imported package can still run code in its init functions.
| Control | What it does | Common mistake |
|---|---|---|
go.mod and go.sum committed | Pins versions and hashes; changes go through code review | Running go get -u ./... in CI so versions drift without review |
Checksum database (GOSUMDB) | Detects a module version whose content changed after first publication | Setting GONOSUMDB, GOINSECURE or GOSUMDB=off broadly instead of listing private module paths in GOPRIVATE |
| Module proxy | Serves cached, consistent module versions even if the source repository disappears | Pointing GOPROXY at an unvetted internal mirror with no integrity checks |
govulncheck ./... | Checks the Go vulnerability database and reports only vulnerable functions your code actually reaches | Relying on version-only scanners that flag unreachable code and get ignored |
| Go toolchain updates | Security fixes ship in point releases (for example 1.25.x) | Pinning an old minor release in Dockerfiles for years |
- Vulnerability data: the Go vulnerability database is fed by the NVD, the GitHub Advisory Database and maintainer reports, curated by the Go Security team and published in OSV format. govulncheck traces calls from your code to the affected symbols, which keeps the noise low enough that teams act on it. The Go team provides a GitHub Action, and
-jsonoutput integrates with other CI systems. - Review updates: the Go team's own guidance is to review and test each dependency update, because an update can bring breaking changes or malicious code. Typosquatted and repository-hijacked modules are a live risk. See the Malicious Package and Dependency Confusion Guide and the SBOM Guide.
- Security announcements: subscribe the team to golang-announce, where Go security releases are pre-announced.
Testing and tooling in the pipeline
- Baseline (week 1): add
go vet ./...,govulncheck ./...andgo test -race ./...to CI on every pull request. Start govulncheck in report-only mode for a sprint, triage, then fail builds on reachable findings. - Static analysis (weeks 2 to 4): add a security linter such as gosec or staticcheck in audit mode, tune out false positives with justified suppressions, then gate new code only (diff-based) so legacy findings do not block delivery.
- Pattern bans (month 2): add lint rules or a custom analyzer for
text/templatein HTTP packages,math/randin security packages,sh -c,InsecureSkipVerifyandhttp.DefaultClient. Go 1.25's new vet analyzers (waitgroup,hostport) catch further concurrency and addressing mistakes. - Fuzzing (month 2 onwards): write native fuzz tests (
func FuzzXxx(f *testing.F), run withgo test -fuzz) for parsers, decoders and anything handling untrusted bytes. Commit the crashing inputs the fuzzer finds as regression corpus. See the Fuzzing Guide. - cgo and unsafe review (ongoing): keep an inventory of packages using cgo or
unsafe, require a second reviewer for changes there, and build cgo test binaries withgo build -asanto catch memory errors in the C side. - Rollback: every gate should have an owner and a documented bypass (a labelled pull request with security approval) so a false positive never blocks an urgent fix, and bypasses are reviewed weekly.
Small teams can stop at steps 1, 3 and 4 and get most of the value. Large organisations should centralise the linter configuration and the govulncheck policy in a shared CI template, and track exceptions centrally. For AI-assisted code, the same gates apply; see the AI-Generated Code Review Guide.
Detection and measurement
- Reachable vulnerability age: median days from a govulncheck finding to a merged fix, against the remediation time frames you document (OWASP ASVS 5.0 V15.1.1 and V15.2.1).
- Toolchain currency: percentage of production services built with a supported, current Go point release.
- Race-tested coverage: share of services whose CI runs
go test -race, and the number of race reports per month (should trend to zero, not be suppressed). - Banned pattern count: occurrences of
text/templatein HTTP code,InsecureSkipVerify,sh -cand unbounded clients, tracked down to zero. - Fuzz coverage: number of parsers and decoders with fuzz tests, and fuzzer crashes fixed per quarter.
- Runtime signals: log and alert on
MaxBytesErrorrates, server timeout counts, recovered panics and 403s from cross-origin protection, which show both attacks and misconfiguration.
Common failure patterns
- The wrong template import: a handler builds HTML with
text/template, or wraps user content intemplate.HTMLto "fix" double escaping, and ships stored XSS. - Default clients and servers in production:
http.ListenAndServeandhttp.Getcopied from examples, with no timeouts, so one slow upstream exhausts goroutines and memory. - Trusting filepath.Join: download and upload handlers that join a user-supplied name onto a base directory and assume
Cleanmade it safe, leaking configuration files or overwriting binaries. - Shelling out for convenience:
exec.Command("sh", "-c", "tool " + name)to get globbing or pipes, turning a safe API into command injection. - Unchecked crypto choices:
math/randfor password-reset tokens,==for API key comparison, andInsecureSkipVerifyleft in from a test environment. - Old toolchains: container images pinned to an unsupported Go minor release, so standard library fixes in
net/http,crypto/tlsorarchive/zipnever reach production, even when govulncheck reports them.
Where identity fits
Go is the language of much cloud infrastructure: operators, CLIs, controllers and agents that hold Kubernetes service account tokens, cloud credentials and API keys. Load these through workload identity rather than embedding them, scope them to the minimum the component needs, and keep them out of logs, panics and error messages. See the Git Secret Scanning Guide for catching keys before they reach a repository.
Framework mapping
| Practice in this guide | OWASP ASVS 5.0 | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 |
|---|---|---|---|
| Parameterised SQL, no shell commands | V1.2.4, V1.2.5 | SI-10 | PR.PS-06 |
| Templates never built from untrusted input | V1.3.7 | SI-10 | PR.PS-06 |
| os.Root and safe archive extraction | V5.3.2, V5.3.3 | SI-10 | PR.PS-06 |
| crypto/rand for non-guessable values | V11.5.1 | SA-8 | PR.PS-06 |
| Race-free shared state | V15.4.1 | SA-11 | PR.PS-06 |
| vet, static analysis, fuzzing in CI | n/a | SA-11, SA-11(1), SA-15 | PR.PS-06 |
| govulncheck, module checksums, update time frames | V15.1.1, V15.2.1, V15.2.4 | SA-15 | ID.RA-01 |
In ISO/IEC 27001:2022 these practices sit under Annex A 8.28 (secure coding), with testing under 8.29; in CIS Controls v8.1 they fall under Control 16, Application Software Security. Web-facing Go services that skip them are the target of MITRE ATT&CK T1190 Exploit Public-Facing Application, and module tampering maps to T1195.001 Compromise Software Dependencies and Development Tools.
Practitioner checklist
- Run
go vet ./...,go test -race ./...andgovulncheck ./...on every pull request, failing on reachable vulnerabilities. - Build every service with a supported, current Go point release and subscribe to golang-announce.
- Replace
http.ListenAndServe,http.Getandhttp.DefaultClientwith configured servers and clients that set timeouts and body limits. - Ban
text/templatein HTTP packages and review every conversion totemplate.HTML,template.JSortemplate.URL. - Use placeholders for all SQL and allow-lists for identifiers; ban
sh -cwith variable input. - Serve and write user-named files only through
os.Rootoros.OpenInRoot, with size and count limits on archive extraction. - Use
crypto/randfor every token andsubtle.ConstantTimeComparefor every secret comparison; removeInsecureSkipVerify. - Keep
go.modandgo.sumin review, leave the checksum database on, and scopeGOPRIVATEto private module paths only. - Add native fuzz tests for every parser and decoder of untrusted input, and inventory all
unsafeand cgo use.
Standards and references
- Go: Security Best Practices for Go Developers
- Go: Go Vulnerability Management
- Go Blog: How Go Mitigates Supply Chain Attacks
- Go Blog: Traversal-resistant file APIs
- Go: html/template package documentation
- Go: FIPS 140-3 Compliance
- OWASP: Application Security Verification Standard 5.0
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Secure Coding Guide · C and C++ Secure Coding Guide · Rust Security Guide · Secure Code Review Guide · Software Supply Chain Security Guide