Frontend Labs
⋯

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 |

Generated from week16.md · 8 Oct 2026 16:36