Overview
The OWASP concerns are universal; what changes in Go is where the defence lives. Some of
it the language design quietly solved (html/template's contextual escaping is one of the
best XSS answers in any ecosystem; no reflective deserialisation gadget chains), some of it
is a one-line library (rs/cors, gorilla/csrf), and some of it is entirely on you
because no framework sets defaults — security headers being the canonical example, where
Spring users get sane values for free and Go users get nothing until they add middleware.
The general taxonomy lives in AppSec fundamentals;
this note is the Go-specific defence per concern.
Key points
- Injection:
database/sqlplaceholders ($1/?) end SQL injection; the absence of ORM magic means there is also nowhere to hide string concatenation — grep forfmt.SprintffeedingQueryand you have your audit. - XSS:
html/templateauto-escapes contextually (HTML body vs attribute vs JS vs URL);text/templatedoes not escape at all and is the classic footgun when someone reaches for it to render HTML. - CSRF:
gorilla/csrffor cookie-session apps; for pure bearer-token APIs CSRF is largely moot, andSameSite=Lax/Strictcookies have downgraded (not eliminated) the risk elsewhere — judge per app. - CORS:
rs/corsasnet/httpmiddleware, or the per-framework equivalents (gin-contrib/cors, echo'smiddleware.CORS); never reflect arbitrary origins with credentials enabled. - Headers:
unrolled/secure(HSTS, CSP, frame/content-type options) or ten lines of hand-rolled middleware — but it must exist; nothing is emitted by default. - Sessions:
gorilla/sessions(cookie or store-backed) or the more modernalexedwards/scs(server-side, pluggable stores, sane renewal API); regenerate the session ID on login either way. - File paths:
filepath.Cleanalone does not stop traversal; useos.Root(Go 1.24) to confine all file operations under a directory, orcyphar/filepath-securejoinbefore that. - SSRF: validate/allowlist target hosts and control the dialer — resolve-then-check
is bypassable via DNS rebinding, so enforce in
net.Dialer.Controlon the actual connected IP. - Resource limits:
http.MaxBytesReaderon request bodies, explicit limits when decompressing client-supplied gzip/zip (decompression bombs), andServertimeouts (ReadHeaderTimeoutet al.) which are unset by default.
Details
Injection beyond SQL
database/sql (and pgx natively) parameterise values, not identifiers — table/column
names still need an allowlist. Query builders (squirrel) and generators (sqlc) keep
parameterisation while composing dynamically. The same discipline generalises:
os/exec.Command takes an argv slice, so shell injection only appears when someone
invokes sh -c with concatenated input; LDAP/NoSQL injection follow the usual encoding
rules from AppSec fundamentals.
html/template's contextual model
The escaper parses the template, determines the context of every interpolation point, and
applies the right encoding chain (htmlEscaper, jsValEscaper, urlFilter, …). Typed
bypasses — template.HTML, template.JS, template.URL — are the explicit "I promise
this is safe" markers; audit every constructor of those types. Remaining foot-guns:
rendering user HTML on purpose (sanitise with microcosm-cc/bluemonday first), and
template injection — never build a template string from user input (template.New(...). Parse(userInput) hands over the escaper itself); user data belongs in the data argument,
not the template text.
CSRF and sessions in practice
gorilla/csrf issues a masked double-submit token tied to the session and rejects
mutating requests without it; wire it around the router and expose the token via
csrf.Token(r)/TemplateField. For APIs authenticated by Authorization headers there
is no ambient credential, hence no CSRF — the judgement call is hybrid apps where a cookie
session backs a JSON API: keep CSRF there, or move fully to SameSite=Strict plus
re-authentication for sensitive actions. With scs, LoadAndSave middleware handles
cookie lifecycle; call RenewToken on privilege change to prevent session fixation.
The unset-by-default tax
Worth a literal checklist because nothing supplies it (contrast Spring Security's defaults):
| Concern | Fix |
|---|---|
| Security headers | unrolled/secure or hand-rolled: HSTS, CSP, X-Content-Type-Options: nosniff, X-Frame-Options/frame-ancestors, Referrer-Policy |
| Server timeouts | set ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout on http.Server |
| Body size | http.MaxBytesReader(w, r.Body, n) before decoding |
| Decompression | wrap gzip readers with io.LimitReader; check zip entry sizes before extraction |
| Error leakage | never http.Error(w, err.Error(), 500) with internal errors |
Examples
// Path traversal: confine file access with os.Root (Go 1.24+).
root, err := os.OpenRoot("/var/app/uploads")
if err != nil { return err }
defer root.Close()
f, err := root.Open(userSuppliedName) // cannot escape /var/app/uploads, symlinks included
// SSRF: enforce on the connected IP, not the looked-up name.
dialer := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
host, _, _ := net.SplitHostPort(address)
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) {
return fmt.Errorf("blocked internal address %s", ip)
}
return nil
},
}
client := &http.Client{Transport: &http.Transport{DialContext: dialer.DialContext}}
Related
- Security in Go applications — the map — parent map of concern → library.
- AppSec fundamentals — the OWASP taxonomy this note instantiates in Go.
- OAuth2 & OIDC in Go — the authentication perimeter these defences sit behind.
- net/http — the server all this middleware wraps, including its unset timeouts.
- Web security in Java apps — the contrast: framework-supplied defaults vs Go's explicit checklist.