If you're preparing for a Go security audit, it's worth focusing on areas where Go's design is safe by default, but subtle mistakes still occur. Here's a practical checklist.
1. Error handling
Go encourages explicit error handling, but ignored errors remain one of the most common issues.
Look for:
_ = someFunc() or result, _ := ...
- Deferred cleanup errors that are silently ignored when they matter
- Missing checks after:
- JSON parsing
- File operations
- Cryptographic functions
- Database queries
- HTTP requests
Example:
data, _ := os.ReadFile("config.json") // ignored error
Instead:
data, err := os.ReadFile("config.json")
if err != nil {
return err
}
2. SQL injection
Use parameterized queries everywhere.
Safe:
db.Query(
"SELECT * FROM users WHERE id = ?",
id,
)
Unsafe:
query := fmt.Sprintf(
"SELECT * FROM users WHERE id=%s",
userInput,
)
Also inspect:
- dynamic ORDER BY
- LIMIT
- table names
- column names
These can't usually be parameterized and require allowlists.
3. Command injection
Never pass user input into a shell.
Bad:
exec.Command("sh", "-c", userInput)
Better:
exec.Command("git", "status")
or validate every argument before use.
4. Path traversal
Watch for:
os.Open("/uploads/" + filename)
An attacker may supply:
../../etc/passwd
Prefer:
filepath.Clean
- allowlisted directories
- verifying the final resolved path remains inside the intended directory.
5. HTTP timeouts
Missing timeouts can enable denial-of-service attacks.
Bad:
http.Client{}
Better:
client := http.Client{
Timeout: 10 * time.Second,
}
Also configure:
- Transport timeouts
- Server ReadTimeout
- WriteTimeout
- IdleTimeout
6. Request body limits
Avoid reading arbitrary request bodies.
Bad:
body, _ := io.ReadAll(r.Body)
Better:
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
7. Goroutine leaks
Look for goroutines that never terminate.
Example:
go func() {
for {
msg := <-ch
process(msg)
}
}()
If ch never closes or the goroutine lacks cancellation, it may leak indefinitely.
Prefer:
context.Context
- cancellation
- closing channels appropriately
8. Context propagation
Database and HTTP operations should usually accept a context.
Instead of:
db.Query(...)
Prefer:
db.QueryContext(ctx, ...)
This improves resilience by allowing cancellation and timeouts.
9. Cryptography
Avoid:
- MD5
- SHA-1 for security-sensitive uses
- homemade encryption
- predictable random values
Use:
crypto/rand
crypto/tls
golang.org/x/crypto
bcrypt or Argon2 for password hashing
Never use:
math/rand
for:
- tokens
- session IDs
- passwords
- API keys
10. TLS configuration
Avoid:
- disabled certificate verification
- outdated TLS versions
Red flag:
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
}
11. Unsafe package
Review every use of:
unsafe.Pointer
Ask:
- Is it necessary?
- Does it violate memory assumptions?
- Could future Go releases change behavior?
Most applications shouldn't require unsafe.
12. Race conditions
Run:
go test -race ./...
The race detector frequently finds:
- shared maps
- shared slices
- cache mutations
- global variables
- concurrent writes
13. Map concurrency
Regular Go maps are not safe for concurrent read/write access.
Bad:
m[key] = value
while another goroutine reads from m.
Use:
sync.RWMutex
sync.Map (when appropriate)
- immutable data structures where practical
14. Secrets in logs
Search for logging of:
- Authorization headers
- JWTs
- passwords
- API keys
- OAuth tokens
- session IDs
Be cautious with %+v on request or configuration structs, as it can expose sensitive fields.
15. JSON unmarshalling
Large or unexpected payloads can cause issues.
Review:
- unknown fields
- oversized payloads
- deeply nested objects
For stricter decoding:
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
16. File permissions
Avoid creating sensitive files with overly permissive modes.
Example:
os.WriteFile(name, data, 0666)
Prefer:
0600
for secrets.
17. Integer overflow
Pay attention when converting between integer types.
Example:
size := int(userValue)
A large uint64 may overflow or truncate when converted to int, potentially affecting allocation sizes or bounds checks.
18. Resource exhaustion
Watch for:
- unbounded goroutine creation
- unlimited queues
- unrestricted uploads
- unconstrained JSON parsing
- large decompression operations (zip bombs)
Limit concurrency with worker pools or semaphores where appropriate.
19. Dependency hygiene
Review:
- vulnerable module versions
- indirect dependencies
- replace directives in
go.mod
- unused dependencies
Useful commands:
go list -m all
go mod tidy
go mod verify
Also run a vulnerability scan with the Go vulnerability checker:
govulncheck ./...
20. Authentication and authorization
Common findings include:
- authentication performed but authorization omitted
- trusting user-controlled IDs
- missing ownership checks
- insecure JWT validation (for example, failing to validate issuer, audience, expiration, or accepted signing algorithms)
Always validate permissions on the server side, even if the client hides unauthorized actions.
Helpful audit tools
go test -race ./... — Detects data races.
govulncheck ./... — Checks your code and dependencies for known Go vulnerabilities.
gosec ./... — Performs static analysis for common Go security issues.
staticcheck ./... — Finds correctness issues, some of which have security implications.
go vet ./... — Identifies suspicious constructs and potential bugs.
A strong audit also includes reviewing application-specific concerns such as authentication, authorization, business logic, and deployment configuration, since many impactful vulnerabilities arise from those layers rather than from Go itself.