OWASP Top 10 2021 for Go APIs (with Hands-On Examples)
OWASP Top 10 2021 is the “greatest hits” list of web app risks. For Go backends and APIs, each item shows up in very specific, repeatable ways.
I also maintain a hands-on lab repo you can play with as you read:
👉 Code lab: go-api-security – “OWASP TOP 10 With GO”
This article is the map: it walks through each OWASP 2021 category in Go terms and points to the repo as the playground.
OWASP Top 10 2021 categories:
- A01: Broken Access Control
- A02: Cryptographic Failures
- A03: Injection
- A04: Insecure Design
- A05: Security Misconfiguration
- A06: Vulnerable and Outdated Components
- A07: Identification and Authentication Failures
- A08: Software and Data Integrity Failures
- A09: Security Logging and Monitoring Failures
- A10: Server-Side Request Forgery
A01: Broken Access Control (Go Edition)
What it is:
Users can do things they shouldn’t: read others’ data, act as admins, bypass checks.
Typical Go smells
- Handlers trust IDs in URL instead of checking ownership.
- Auth is in middleware “most of the time” but some routes skip it.
- Multi-tenant app:
org_idis passed from client and never validated.
Vulnerable pattern
// GET /users/{id}
func getUser(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id") // from URL
// current user is authenticated as userID in context but we ignore it
user, _ := dbGetUserByID(r.Context(), id)
json.NewEncoder(w).Encode(user) // ❌ any user can fetch any ID
}
Safer pattern
type AuthedUser struct {
ID string
OrgID string
Role string
}
func currentUser(r *http.Request) *AuthedUser {
u, _ := r.Context().Value(userKey{}).(*AuthedUser)
return u
}
func getUser(w http.ResponseWriter, r *http.Request) {
u := currentUser(r)
if u == nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
id := chi.URLParam(r, "id")
user, err := dbGetUserByID(r.Context(), id)
if err != nil {
http.Error(w, "not found", http.StatusNotFound)
return
}
if user.OrgID != u.OrgID && u.Role != "admin" {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
json.NewEncoder(w).Encode(user)
}
Checklist for Go devs
- Use auth middleware that loads the user into
context.Context. - Always derive permissions from server-side identity, not from client-supplied IDs.
- Enforce tenant boundaries (
OrgID,AccountID) on every data access. - Use deny by default routing: sensitive routes must opt in to access, not opt out.
👉 Code: go-api-security has runnable access control examples to experiment with.
A02: Cryptographic Failures
What it is: Bad or missing crypto: weak tokens, broken password hashing, unencrypted data in transit or at rest.
Typical Go smells
- Using
math/randfor API keys or password reset tokens. - Homegrown password hashing (
sha256(password)) instead ofbcrypt/argon2. - Skipping TLS verification in
http.Clientor accepting any cert.
Vulnerable pattern
// Token that looks random but is predictable
func insecureToken() string {
// math/rand is predictable if seed is known
return fmt.Sprintf("%d", rand.Int63()) // ❌ not cryptographically secure
}
Safer pattern – random tokens
import "crypto/rand"
import "encoding/hex"
func secureToken(nBytes int) (string, error) {
b := make([]byte, nBytes)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
Safer pattern – password hashing
import "golang.org/x/crypto/bcrypt"
func hashPassword(pw string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
return string(hash), err
}
func checkPassword(hash, pw string) error {
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw))
}
Checklist for Go devs
- Use
crypto/*packages, notmath/randor DIY crypto. - Hash passwords with
bcryptorargon2id, never raw hash functions. - Require TLS for all external communication; avoid
InsecureSkipVerifyintls.Config.
👉 Code: the repo includes concrete examples and exercises for password storage and token generation.
A03: Injection (incl. SQL, Command Injection, XSS basics)
What it is: Untrusted data gets interpreted as code: SQL, shell, template expressions, etc. Classic XSS now sits under this umbrella.
Typical Go smells
- Building SQL statements with
fmt.Sprintfand string concatenation. - Passing user input into
exec.Command("sh", "-c", "..."). - Using
text/templateinstead ofhtml/templatein HTML responses.
Vulnerable SQL pattern
func searchUsers(ctx context.Context, q string) ([]User, error) {
// ❌ user input directly in SQL string
sql := fmt.Sprintf(`SELECT id, email FROM users WHERE email LIKE '%%%s%%'`, q)
rows, err := db.QueryContext(ctx, sql)
// ...
}
Safer SQL pattern (parameterized)
func searchUsers(ctx context.Context, q string) ([]User, error) {
rows, err := db.QueryContext(ctx,
`SELECT id, email FROM users WHERE email LIKE ?`, "%"+q+"%",
)
// ...
}
Vulnerable command pattern
func runTool(input string) error {
// ❌ shell expands input, potential command injection
cmd := exec.Command("sh", "-c", "tool "+input)
return cmd.Run()
}
Safer command pattern
func runTool(arg string) error {
// ✅ no shell, arguments treated as literal
cmd := exec.Command("tool", arg)
return cmd.Run()
}
For XSS in Go templates, stick to html/template and avoid template.HTML on user input (I have a separate “XSS with Go” write-up that goes deep on this).
Checklist
- Always use parameterized queries (
?placeholders, no string concatenation). - Avoid
sh -cinexec.Command; pass args as separate parameters. - Use
html/templatefor HTML, nevertext/templatefor web views. - Treat any raw HTML or script from users as hostile and sanitize it.
👉 Code: see injection-focused examples in go-api-security and pair them with the XSS article.
A04: Insecure Design
What it is: The design itself is unsafe even if the code has no obvious bugs: missing rate limits, risky “magic links”, insecure workflows.
Go-shaped examples
- Login endpoint allows unlimited attempts, no account lockout.
- Password reset magic links never expire and are reusable.
- Admin actions don’t require elevated checks or audit logging.
In earlier posts I walked through Threat Sketches and lightweight threat modeling for tiny teams. This is where those practices start paying off.
Example: login without rate limiting
func login(w http.ResponseWriter, r *http.Request) {
// Perfectly “correct” code, but no throttling / lockout.
}
More secure design sketch
- Rate limit login per IP + per account.
- Add small backoff on repeated failures.
- Log failed logins (without passwords) and monitor patterns.
Design lives one level up from code. The book/ content in go-api-security walks through Insecure Design vs better design in Go form.
Checklist
For each critical feature (login, password reset, invoice sharing), ask:
- “How could this be abused at scale?”
- “What happens if someone automates this 10k times?”
Add rate limiting, expiries, and revocation to designs.
Use the 10-minute Threat Sketch habit for
sec:highstories.
A05: Security Misconfiguration
What it is: Settings and defaults that leave the app exposed: debug endpoints, open admin interfaces, sloppy CORS.
Typical Go smells
- Exposing
/debug/pprofor/debug/varson the public internet. - Default
http.Serverwith no timeouts or limits. Access-Control-Allow-Origin: *with credentials.- Frameworks running in debug mode in production.
Vulnerable pattern
func main() {
mux := http.NewServeMux()
mux.Handle("/", appHandler)
// ❌ pprof exposed on same listener
mux.Handle("/debug/pprof/", http.DefaultServeMux)
http.ListenAndServe(":8080", mux)
}
Safer pattern
func main() {
appMux := http.NewServeMux()
appMux.Handle("/", appHandler)
srv := &http.Server{
Addr: ":8443",
Handler: appMux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))
}
Expose pprof/metrics:
- On a separate listener bound to localhost or internal network.
- Or behind auth / VPN / IP allowlist.
Checklist
- Turn off debug features in production (pprof, default expvar, framework debug mode).
- Configure
http.Servertimeouts and reasonable body size limits. - Use explicit, minimal CORS configuration; avoid wildcard with cookies.
- Protect admin/UIs with auth and, if possible, separate network paths.
👉 Code: the repo’s capstone script (capstone_e2e.sh) glues everything together; it’s a nice place to think about hardening misconfigurations end-to-end.
A06: Vulnerable and Outdated Components
What it is: Using libraries, frameworks, or tools with known vulnerabilities.
Go ships some useful tooling here.
govulncheck– the official vulnerability scanner that looks at your modules and flags reachable vulns.gosec– static analysis for Go, scanning source for security issues.
Example: running govulncheck on the repo
go install golang.org/x/vuln/cmd/govulncheck@latest
cd go-api-security
govulncheck ./...
govulncheck uses the Go vulnerability database to report known CVEs that affect your dependencies.
Checklist
- Check
go.modandgo.suminto version control. - Add
govulncheckto CI for./.... - Patch dependencies on a regular cadence (weekly/biweekly).
- Pin base Docker images and monitor them for CVEs.
- Don’t blindly update: review changelogs and rerun tests.
👉 Code: clone go-api-security and run govulncheck and gosec against it to see what these tools look like in practice.
A07: Identification & Authentication Failures
What it is: Login, session, or token problems: weak flows, bypasses, or missing checks.
Typical Go smells
- Parsing a JWT’s claims but never verifying the signature.
- Accepting
alg: noneor wrong signing algorithm. - Password reset tokens never expire and are reused.
- No rate limiting on login or 2FA.
Vulnerable JWT pattern
func currentUserFromJWT(tokenStr string) (*UserClaims, error) {
token, _, err := new(jwt.Parser).ParseUnverified(tokenStr, &UserClaims{})
if err != nil {
return nil, err
}
claims, _ := token.Claims.(*UserClaims)
return claims, nil // ❌ never verify signature
}
Safer JWT pattern
func parseAndVerifyJWT(tokenStr string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &UserClaims{}, func(t *jwt.Token) (any, error) {
// Enforce expected signing method
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method")
}
return []byte(hmacSecret), nil
})
if err != nil || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, fmt.Errorf("bad claims")
}
return claims, nil
}
Checklist
Use well-tested auth libraries and validate:
- Algorithm, issuer, audience, expiry, not-before.
Rate limit login and 2FA attempts.
Rotate session IDs / tokens on login and privilege change.
Make password reset tokens single-use and short-lived.
👉 Code: the auth/login examples in the repo are a natural place to wire these checks in and experiment.
A08: Software and Data Integrity Failures
What it is: Trusting unverified code or data: compromised pipeline, malicious dependencies, untrusted serialized objects.
Go-shaped examples
- CI pipeline that runs
go install github.com/some/random/tool@latestwithout pinning or review. - Application downloading plugins or configs from arbitrary URLs at startup.
- Accepting config or policy data from the client and using it directly for authorization decisions.
Vulnerable pattern
# In CI: always install latest from internet
steps:
- run: go install github.com/example/tool@latest # ❌ if account is compromised, you pull malicious code
Safer pattern
Pin versions in CI:
go install github.com/example/tool@v1.2.3
Review and update explicitly instead of “latest”.
Checklist
- Pin versions for build tools and modules.
- Avoid dynamic loading of code from untrusted sources.
- Don’t let clients control security-critical config; keep policies on the server.
- Use signed releases/checksums for important artifacts.
👉 Code: the capstone_e2e.sh and CI examples around go-api-security are a good thought exercise for software supply chain and integrity.
A09: Security Logging & Monitoring Failures
What it is: No (or bad) logging and monitoring so you can’t detect or investigate attacks.
Typical Go smells
- Using only
log.Printlnwith no request IDs or structure. - Logging secrets (
Authorizationheaders, tokens, passwords). - No logs for access control failures, only 500s.
Simple logging middleware
type keyRequestID struct{}
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.NewString()
ctx := context.WithValue(r.Context(), keyRequestID{}, id)
r = r.WithContext(ctx)
log.Printf("request_id=%s method=%s path=%s", id, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
You can upgrade to structured loggers (zap, zerolog, logrus) but even this is better than nothing.
Checklist
Log at least:
- Auth events (login success/fail, password reset).
- Access control failures (403).
- Significant data changes (delete account, change email, privilege changes).
Never log passwords, full tokens, or secrets.
Centralize logs and build basic alerts (spikes in 401/403, unusual IP ranges).
👉 Code: wire a logging middleware into the HTTP stack in go-api-security and watch how much easier incidents become to reason about.
A10: Server-Side Request Forgery (SSRF)
What it is: The app makes HTTP requests based on user input; an attacker makes it call internal services or metadata endpoints.
Typical Go smells
- “URL fetcher” or “webhook tester” endpoints that just do
http.Get(userURL). - PDF/image “proxy” that loads from arbitrary URLs under your server’s network identity.
Vulnerable pattern
func fetchURL(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("url") // user-controlled
resp, err := http.Get(target) // ❌ can hit 169.254.169.254, internal IPs, etc.
if err != nil {
http.Error(w, "fetch error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
io.Copy(w, resp.Body)
}
Safer pattern (allowlist + custom transport)
var allowedHosts = map[string]bool{
"example.com": true,
"api.partner.com": true,
}
func isAllowed(u *url.URL) bool {
return allowedHosts[u.Hostname()]
}
func safeClient() *http.Client {
tr := &http.Transport{
// Optionally restrict IP ranges here (block 10.0.0.0/8, 169.254.169.254, etc.)
// via DialContext wrapper.
}
return &http.Client{Transport: tr, Timeout: 5 * time.Second}
}
func fetchURL(w http.ResponseWriter, r *http.Request) {
raw := r.URL.Query().Get("url")
u, err := url.Parse(raw)
if err != nil || u.Scheme != "https" {
http.Error(w, "bad url", http.StatusBadRequest)
return
}
if !isAllowed(u) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
client := safeClient()
resp, err := client.Get(u.String())
if err != nil {
http.Error(w, "fetch error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
io.Copy(w, io.LimitReader(resp.Body, 1<<20)) // 1MB limit
}
Checklist
- Never call
http.Geton raw user input without validation. - Restrict outbound hosts via allowlists or tight DNS/IP controls.
- Add timeouts and response body size limits when proxying.
- Avoid exposing generic “fetch any URL” features; prefer specific integrations.
👉 Code: there’s a natural SSRF playground in the “URL fetcher” style examples in go-api-security.
Tooling: Go-Native Help for OWASP Top 10
It’s easy to wire simple security tooling into CI for Go projects:
- gosec – SAST for Go that scans code for security problems.
- govulncheck – the official Go vulnerability checker using the Go vuln DB.
Example GitHub Actions snippet:
name: security-checks
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Run govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
- name: Run gosec
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...
Run that against go-api-security or against your own services to get quick feedback.
OWASP Top 10 Checklist for Go APIs
Here’s a concise, Go-flavored checklist summarizing everything:
OWASP Top 10 for Go APIs – Cheat Sheet
A01 Broken Access Control
- Every handler enforces auth/authorization based on server-side identity, not client IDs.
- Tenant/org boundaries are checked on every data access.
- Sensitive routes are protected via middleware (deny-by-default).
A02 Cryptographic Failures
- Only
crypto/*packages used for tokens/crypto, nevermath/rand. - Passwords hashed with bcrypt/argon2, never plain or DIY hashes.
- TLS is enforced; no
InsecureSkipVerifyin production.
A03 Injection
- All SQL queries are parameterized (no string concatenation).
- No
exec.Command("sh", "-c", ...)on user input. - HTML rendering uses
html/templatewith no rawtemplate.HTMLfrom users.
A04 Insecure Design
- Critical flows (login, reset, sharing links) have rate limits, expiries, and revocation.
- A 5–10 minute threat sketch exists for
sec:highstories.
A05 Security Misconfiguration
- Debug endpoints (/debug/pprof, /metrics) are not exposed publicly.
-
http.Servertimeouts and limits are configured. - CORS configured minimally; no
*with credentials.
A06 Vulnerable & Outdated Components
-
govulncheckruns regularly in CI. - Dependencies and base images are pinned and updated on a schedule.
A07 Identification & Authentication Failures
- JWTs are fully verified (alg, issuer, audience, expiry).
- Login has brute-force protections and secure password reset.
- Tokens/sessions are rotated on login and privilege change.
A08 Software & Data Integrity Failures
- Build tools and modules are version-pinned (no “@latest” in CI).
- No dynamic code loading from untrusted URLs.
- Clients don’t control security-critical config.
A09 Security Logging & Monitoring Failures
- Requests have IDs; important auth/permission events are logged.
- Secrets/tokens are never logged.
- There are basic alerts on auth failures and anomalies.
A10 Server-Side Request Forgery
- No generic
http.Geton user-supplied URLs. - Outbound requests restricted by allowlist or tight IP/DNS policy.
- Proxies have timeouts and size limits.
If you want to turn this from theory into muscle memory, clone go-api-security, run the examples locally, and start layering these habits into your real Go services.