Day 115 — Security Audit of Your Own Code
| Learning objective | Systematically audit all security-critical code paths for common vulnerabilities |
| Key packages | crypto/subtle, net/http |
| Security context | Adversarial review of your own code is the last line of defense before deployment |
Audit Checklist
1. Timing attacks — all secret comparisons must use subtle.ConstantTimeCompare
// BAD — timing oracle: loop exits early on first mismatch
if apiKey == expected {
// ...
}
// BAD — string comparison is not constant-time in Go
if hmac.Equal([]byte(provided), []byte(expected)) {} // hmac.Equal IS constant-time — OK
// GOOD — always takes same time regardless of where mismatch occurs
import "crypto/subtle"
if subtle.ConstantTimeCompare([]byte(provided), []byte(expected)) != 1 {
// reject
}
// GOOD — for HMAC comparison
if !hmac.Equal(computedMAC, expectedMAC) {
// reject
}
2. Integer overflow in packet parsers
// BAD — attacker sends length=4294967295, allocation panics or wraps
func parsePacket(data []byte) []byte {
length := binary.BigEndian.Uint32(data[:4])
payload := make([]byte, length) // panic if length > maxInt
copy(payload, data[4:4+length]) // out of bounds if length > len(data)-4
return payload
}
// GOOD — bounds check before allocation
const maxPacketSize = 1 << 20 // 1MB
func parsePacket(data []byte) ([]byte, error) {
if len(data) < 4 {
return nil, errors.New("truncated header")
}
length := binary.BigEndian.Uint32(data[:4])
if length > maxPacketSize {
return nil, fmt.Errorf("packet length %d exceeds maximum %d", length, maxPacketSize)
}
if int(length) > len(data)-4 {
return nil, fmt.Errorf("packet claims %d bytes but only %d available", length, len(data)-4)
}
payload := make([]byte, length)
copy(payload, data[4:4+length])
return payload, nil
}
3. Error messages must not leak internals
// BAD — leaks internal path
http.Error(w, fmt.Sprintf("file not found: /var/app/data/%s", filename), 404)
// BAD — leaks stack trace
http.Error(w, err.Error(), 500) // err might be: "dial tcp 10.0.0.5:5432 connection refused"
// GOOD — generic message to client, detailed log internally
requestID := r.Header.Get("X-Request-ID")
logger.Error("upstream connection failed",
"request_id", requestID,
"upstream", upstreamAddr,
"error", err,
)
http.Error(w, fmt.Sprintf(`{"error":"internal_error","request_id":%q}`, requestID), 500)
4. Goroutine exit conditions
// AUDIT CHECKLIST for every goroutine in your codebase:
// [ ] Does it listen on a channel? → Is there a corresponding sender or close?
// [ ] Does it poll in a for loop? → Is there a done/ctx.Done case?
// [ ] Is it started per-connection? → Is it cleaned up when connection closes?
// [ ] Is it a background worker? → Does it respond to graceful shutdown?
// Pattern to audit:
grep -n "^.*go func" **/*.go // find every goroutine start
// For each: trace to the goroutine body and verify the exit condition
Exercise — Security Audit Report Template
## Security Audit: [Component Name]
**Date:** YYYY-MM-DD
**Auditor:** [Your name]
**Files reviewed:** [list of files]
### Finding 1: [Title]
- **Severity:** Critical / High / Medium / Low
- **Location:** file.go:line
- **Finding:** Description of the issue
- **Evidence:** Code snippet showing the vulnerability
- **Fix:** Code snippet showing the fix
- **Status:** Fixed / Accepted Risk / Won't Fix
### Summary
| Severity | Found | Fixed | Accepted |
|----------|-------|-------|---------|
| Critical | 0 | 0 | 0 |
| High | 1 | 1 | 0 |
| Medium | 3 | 3 | 0 |
| Low | 2 | 1 | 1 |