Join our Newsletter — 33% off our NHI Course
Home› Guides› Go Secure Coding Guide: Common Pitfalls and Safe…
Guide Application Security

Go Secure Coding Guide: Common Pitfalls and Safe Patterns

← All guides
By NHI Mgmt Group, edited by Lalit Choda Updated 26 October 2026 17 min read
On this page

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 unsafe and cgo opt out of the guarantees.
  • Pick the safe package, not the similar one. html/template over text/template for HTML, crypto/rand over math/rand, os.Root over filepath.Join with user input, and exec.Command with separate arguments over sh -c.
  • Every network boundary needs explicit limits. Set server timeouts, a request body cap with http.MaxBytesReader, and a client Timeout or 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 -race and treat go vet findings as build failures.
  • Use the toolchain's supply chain controls. Commit go.mod and go.sum, keep the checksum database on, run govulncheck ./... 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 areaWhat Go gives youWhat is still on you
Buffer overflow, use-after-freeBounds checks, garbage collectionCode using unsafe.Pointer, unsafe.Slice or cgo, and C libraries linked through cgo
Integer overflowDefined wrap-around behaviour, no undefined behaviourWrapped lengths and offsets used for allocation or slicing, conversions between int64 and int32
Injection (SQL, OS command, template)Safe APIs exist in the standard libraryUsing them consistently; string concatenation still compiles
ConcurrencyGoroutines and channels, race detectorData races on shared maps and structs; concurrent map writes cause a fatal runtime error
Denial of serviceHeader size default (1 MB), MaxBytesReaderServer and client timeouts, body caps, goroutine and decompression limits
Panicsnet/http recovers panics per requestPanics in goroutines you start yourself crash the whole process
Dependenciesgo.sum, checksum database, no install scriptsChoosing, 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 with fmt.Sprintf or +.
  • Identifiers cannot be parameterised: table names, column names and ORDER BY directions 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/exec intentionally does not invoke the system shell, so exec.CommandContext(ctx, "convert", in, out) passes each argument as a separate string with no globbing, pipes or redirection. The risk returns the moment code calls exec.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.LookPath no longer resolves a program from the current directory and returns an error matching exec.ErrDot instead. Do not re-enable the old behaviour with GODEBUG=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/template in any package that writes HTTP responses.
  • Typed escape hatches: template.HTML, template.JS, template.URL, template.HTMLAttr and template.CSS mark 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 in template.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.

SettingDefaultSafer patternThreat addressed
Server.ReadHeaderTimeoutZero (no limit)A few secondsSlow-header (Slowloris style) connection exhaustion
Server.ReadTimeout / WriteTimeoutZeroSet per service; longer only for streaming handlersSlow bodies and stuck responses holding goroutines
Server.IdleTimeoutFalls back to ReadTimeoutExplicit valueIdle keep-alive connection build-up
Server.MaxHeaderBytesDefaultMaxHeaderBytes (1 MB)Lower if the API never needs large headersMemory pressure from oversized headers
Request body sizeUnlimitedWrap with http.MaxBytesReader(w, r.Body, n); handle *http.MaxBytesErrorMemory exhaustion from large JSON or uploads
Client.TimeoutZero (no timeout)Dedicated client with a timeout, plus context.WithTimeout per callGoroutine leaks when a dependency hangs
Cross-origin POSTsNot checkedhttp.CrossOriginProtection (Go 1.25) or a token schemeCSRF on cookie-authenticated endpoints
  • CSRF: Go 1.25 added http.CrossOriginProtection, which rejects non-safe cross-origin browser requests using the Sec-Fetch-Site Fetch 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.LimitReader around gzip readers), and cap multipart memory with the argument to ParseMultipartForm.
  • JSON strictness: use json.Decoder with DisallowUnknownFields() on security-relevant inputs, and decode into request-specific structs rather than the domain model, so a client cannot set fields such as IsAdmin (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.Dialer Control function, 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 its Open, Create, Stat, Mkdir and Remove methods with relative names, or use os.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 as NUL and COM1 are 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=js the 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 0600 and directories with 0700, and use os.CreateTemp rather 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

TaskUseAvoid
Tokens, session IDs, reset codes, keyscrypto/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 keyscrypto/subtle.ConstantTimeCompare or hmac.Equal== or bytes.Equal on secrets
Password storagegolang.org/x/crypto/argon2 or bcryptPlain SHA-256 or MD5 of the password
Symmetric encryptionAES-GCM via crypto/cipher with a fresh random nonce per message, or x/crypto/chacha20poly1305Unauthenticated modes, reused nonces
TLS clientsDefault crypto/tls configuration, MinVersion of TLS 1.2 or higherInsecureSkipVerify: true outside tests
FIPS 140-3 environmentsNative Go Cryptographic Module (Go 1.24+), selected at build time with GOFIPS140GODEBUG=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.0 pins it; GODEBUG=fips140=on enables 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 %+v on 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.RWMutex or 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.Stat then opening it, or checking a balance then debiting it in two queries, is a race regardless of language. Use atomic operations: open then Stat the handle, and database transactions with row locks or conditional updates.
  • Goroutine panics: net/http recovers 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, errgroup with SetLimit).
  • Error checking: ignored errors from Close, Write or 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 checking err now 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.

ControlWhat it doesCommon mistake
go.mod and go.sum committedPins versions and hashes; changes go through code reviewRunning go get -u ./... in CI so versions drift without review
Checksum database (GOSUMDB)Detects a module version whose content changed after first publicationSetting GONOSUMDB, GOINSECURE or GOSUMDB=off broadly instead of listing private module paths in GOPRIVATE
Module proxyServes cached, consistent module versions even if the source repository disappearsPointing GOPROXY at an unvetted internal mirror with no integrity checks
govulncheck ./...Checks the Go vulnerability database and reports only vulnerable functions your code actually reachesRelying on version-only scanners that flag unreachable code and get ignored
Go toolchain updatesSecurity 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 -json output 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

  1. Baseline (week 1): add go vet ./..., govulncheck ./... and go test -race ./... to CI on every pull request. Start govulncheck in report-only mode for a sprint, triage, then fail builds on reachable findings.
  2. 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.
  3. Pattern bans (month 2): add lint rules or a custom analyzer for text/template in HTTP packages, math/rand in security packages, sh -c, InsecureSkipVerify and http.DefaultClient. Go 1.25's new vet analyzers (waitgroup, hostport) catch further concurrency and addressing mistakes.
  4. Fuzzing (month 2 onwards): write native fuzz tests (func FuzzXxx(f *testing.F), run with go test -fuzz) for parsers, decoders and anything handling untrusted bytes. Commit the crashing inputs the fuzzer finds as regression corpus. See the Fuzzing Guide.
  5. 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 with go build -asan to catch memory errors in the C side.
  6. 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/template in HTTP code, InsecureSkipVerify, sh -c and 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 MaxBytesError rates, 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 in template.HTML to "fix" double escaping, and ships stored XSS.
  • Default clients and servers in production: http.ListenAndServe and http.Get copied 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 Clean made 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/rand for password-reset tokens, == for API key comparison, and InsecureSkipVerify left in from a test environment.
  • Old toolchains: container images pinned to an unsupported Go minor release, so standard library fixes in net/http, crypto/tls or archive/zip never 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 guideOWASP ASVS 5.0NIST SP 800-53 Rev. 5NIST CSF 2.0
Parameterised SQL, no shell commandsV1.2.4, V1.2.5SI-10PR.PS-06
Templates never built from untrusted inputV1.3.7SI-10PR.PS-06
os.Root and safe archive extractionV5.3.2, V5.3.3SI-10PR.PS-06
crypto/rand for non-guessable valuesV11.5.1SA-8PR.PS-06
Race-free shared stateV15.4.1SA-11PR.PS-06
vet, static analysis, fuzzing in CIn/aSA-11, SA-11(1), SA-15PR.PS-06
govulncheck, module checksums, update time framesV15.1.1, V15.2.1, V15.2.4SA-15ID.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 ./... and govulncheck ./... 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.Get and http.DefaultClient with configured servers and clients that set timeouts and body limits.
  • Ban text/template in HTTP packages and review every conversion to template.HTML, template.JS or template.URL.
  • Use placeholders for all SQL and allow-lists for identifiers; ban sh -c with variable input.
  • Serve and write user-named files only through os.Root or os.OpenInRoot, with size and count limits on archive extraction.
  • Use crypto/rand for every token and subtle.ConstantTimeCompare for every secret comparison; remove InsecureSkipVerify.
  • Keep go.mod and go.sum in review, leave the checksum database on, and scope GOPRIVATE to private module paths only.
  • Add native fuzz tests for every parser and decoder of untrusted input, and inventory all unsafe and cgo use.

Standards and references

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

Free weekly newsletter

Subscribe to the NHI & AI Identity Journal

The latest on NHI and Agentic AI security – articles, research, breaches, news and events every week.

Bonus 33% off our NHI Course when you subscribe.

NHIMG Editorial Note
Researched and written by the NHI Mgmt Group editorial team using AI tools, under the editorial direction of Lalit Choda. Facts are drawn from the sources listed on this page and checked by our automated quality process. Last updated 26 October 2026. Spotted an error? Use our contact page.
NHI Mgmt Group — the #1 independent authority on Non-Human Identity, IAM, and Agentic AI security. nhimg.org