| name | go-security-audit |
| description | Security review for Go applications: input validation, SQL injection, authentication/authorization, secrets management, TLS, OWASP Top 10, and secure coding patterns. Use when performing security reviews, checking for vulnerabilities, hardening Go services, or reviewing auth implementations. Trigger examples: "security review", "check vulnerabilities", "OWASP", "SQL injection", "input validation", "secrets management", "auth review". Do NOT use for dependency CVE scanning (use go-dependency-audit) or concurrency safety (use go-concurrency-review).
|
| license | MIT |
| metadata | {"version":"1.2.0"} |
Go Security Audit
Security is not a feature — it's a property. Every line of code either
maintains it or degrades it.
Operating Modes
Pick the mode that matches the request before starting:
- Targeted check — a single concern ("is this query injectable?",
"review this auth middleware"). Apply only the relevant sections.
- Diff audit — audit the changed lines of a PR or working tree for
every concern below.
- Full audit (default for "security review the service") — sweep the
codebase using the parallel passes in "Auditing Large Codebases".
Run the Scanners First
Before manual review, run the automated scanners and fold their output
into the findings (skip any that is not installed and note it):
govulncheck ./...
gosec ./...
go vet ./...
Scanners find the known patterns; the manual passes below find the
logic flaws they cannot.
Auditing Large Codebases
Each numbered section below is an independent audit pass. For codebases
beyond ~20 files:
- Locate the attack surface first: HTTP/gRPC handlers, CLI entry
points, queue consumers, and anything parsing external input.
- Run one pass per concern: (a) input validation + injection,
(b) authentication/authorization, (c) secrets + crypto,
(d) TLS + security headers + rate limiting, (e) logging hygiene.
- If your environment supports delegating work to parallel sub-agents
or tasks, assign each pass to one — the passes don't overlap.
Otherwise run them sequentially.
- Every finding must cite
file.go:line, the vulnerable input path,
and a concrete fix. Aggregate into one report sorted by severity.
1. Input Validation
NEVER trust user input. Validate at the boundary:
func (h *Handler) handleCreate(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req CreateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
if err := validate.Struct(req); err != nil {
respondError(w, http.StatusBadRequest, "validation failed")
return
}
}
String sanitization:
import "github.com/microcosm-cc/bluemonday"
p := bluemonday.UGCPolicy()
sanitized := p.Sanitize(userInput)
import "net/mail"
_, err := mail.ParseAddress(email)
u, err := url.Parse(input)
if err != nil || (u.Scheme != "http" && u.Scheme != "https") {
}
2. SQL Injection Prevention
ALWAYS use parameterized queries:
row := db.QueryRowContext(ctx,
"SELECT id, name FROM users WHERE email = $1", email)
query := "SELECT * FROM users WHERE name = :name AND age > :age"
rows, err := db.NamedQueryContext(ctx, query, map[string]interface{}{
"name": name,
"age": minAge,
})
query := "SELECT * FROM users WHERE email = '" + email + "'"
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)
Dynamic queries:
When building dynamic WHERE clauses, use query builders or safe concatenation:
var conditions []string
var args []interface{}
argIdx := 1
if name != "" {
conditions = append(conditions, fmt.Sprintf("name = $%d", argIdx))
args = append(args, name)
argIdx++
}
query := "SELECT * FROM users"
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
3. Authentication & Authorization
Password handling:
import "golang.org/x/crypto/bcrypt"
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
NEVER store plaintext passwords. NEVER use MD5/SHA for passwords.
JWT validation:
Authorization middleware:
func RequireRole(role string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := UserFromContext(r.Context())
if user == nil || !user.HasRole(role) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
4. Secrets Management
Rules:
- 🔴 NEVER hardcode secrets, tokens, or API keys in source code
- 🔴 NEVER commit secrets to git (even in "test" files)
- 🔴 NEVER log secrets, tokens, or passwords
dbURL := os.Getenv("DATABASE_URL")
secret, err := secretsManager.GetSecret(ctx, "api-key")
const apiKey = "sk-1234567890abcdef"
Use .gitignore:
.env
*.pem
*.key
credentials.json
Scan for leaked secrets:
gitleaks detect --source=. --verbose
5. HTTP Security Headers
func SecurityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self'")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
6. TLS Configuration
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
},
PreferServerCipherSuites: true,
}
srv := &http.Server{
TLSConfig: tlsConfig,
}
7. Rate Limiting
import "golang.org/x/time/rate"
type RateLimiter struct {
limiters sync.Map
rate rate.Limit
burst int
}
func (rl *RateLimiter) Allow(key string) bool {
limiter, _ := rl.limiters.LoadOrStore(key,
rate.NewLimiter(rl.rate, rl.burst))
return limiter.(*rate.Limiter).Allow()
}
Apply rate limiting to auth endpoints, public APIs, and any resource-intensive operations.
8. Logging Security
log.Printf("user login: email=%s password=%s", email, password)
log.Printf("auth token: %s", token)
log.Printf("request body: %v", req)
log.Printf("user login: email=%s", email)
logger.Info("auth completed", slog.String("user_id", userID))
Security Audit Checklist
Critical (🔴 BLOCKER)
- No SQL injection vectors (all queries parameterized)
- No hardcoded secrets/keys/tokens
- No plaintext password storage
- No disabled TLS certificate verification
- Request body size limited
- JWT signature verified,
alg: none rejected
Important (🟡 WARNING)
- Input validation on all external data
- Rate limiting on auth and public endpoints
- Security headers set on all responses
- CORS configured restrictively
- Error messages don't leak internals
- Audit logging for auth events
Recommended (🟢 SUGGESTION)
govulncheck in CI pipeline
gitleaks for secret scanning
- Structured logging with redaction
- Dependency pinning with verified checksums