Perform a security audit of a Go codebase. Targets SSH servers, BBS systems, API services, and CLI tools. Finds race conditions, goroutine leaks, missing err...
---
name: go-security-audit
description: >
Perform a security audit of a Go codebase. Targets SSH servers, BBS systems,
API services, and CLI tools. Finds race conditions, goroutine leaks, missing
error handling, command injection, and auth bypasses. Produces a prioritized
finding list with file:line citations and minimal diff patches.
Use when asked to audit, review, or security-test a Go repository.
---
# Go Security Audit
## Workflow
### Phase 1: Reconnaissance
1. Map the codebase: `go.mod` for dependencies, `cmd/` for entry points, `internal/` for core logic
2. Identify concurrency primitives: `sync.Mutex`, `sync.RWMutex`, channels, goroutines
3. Identify external inputs: HTTP handlers, SSH sessions, CLI args, env vars, DB queries
4. Note subprocess execution: `exec.Command`, `os.StartProcess`
### Phase 2: Race Condition Analysis
Check every shared mutable state:
**TOCTOU patterns — check/act gaps:**
```go
// BAD: unlock between check and use
r.mu.Lock()
entry, ok := r.m[id]
r.mu.Unlock() // ← gap here
if !ok { return }
entry.Close() // ← entry could be gone
// GOOD: defer unlock or keep locked through the operation
r.mu.Lock()
defer r.mu.Unlock()
entry, ok := r.m[id]
if !ok { return }
delete(r.m, id)
entry.Close()
```
**Map concurrent access:**
- Any `map` written in one goroutine and read in another without mutex → data race
- Use `sync.Map` or protect with `sync.RWMutex`
**Channel patterns:**
- Unbuffered channels sent to from goroutines that may outlive the receiver → goroutine leak
- `close()` called on a channel that may already be closed → panic
### Phase 3: Error Handling Audit
Find silent failures:
```go
// Flag patterns:
result, _ = someFunc() // error discarded
res.LastInsertId() // return value ignored
time.Parse(layout, val) // two-return ignored with _
```
Every `_` on the error position should be justified or flagged.
**SQL patterns:**
```go
// Check LastInsertId separately:
id, err := res.LastInsertId()
if err != nil {
return 0, fmt.Errorf("get insert id: %w", err)
}
```
### Phase 4: Command Injection
Check every `exec.Command` / `exec.CommandContext` call:
```go
// Risky: user-controlled input split with strings.Fields
parts := strings.Fields(os.Getenv("USER_CMD"))
cmd := exec.CommandContext(ctx, parts[0], parts[1:]...)
// Safe: validate no shell metacharacters, or use explicit args
if strings.ContainsAny(cmdline, "|;&$`(){}") {
return fmt.Errorf("invalid command")
}
```
### Phase 5: Auth and Session Checks
- Are admin routes protected? Check every handler for auth middleware
- Session IDs: are they random (crypto/rand) or sequential/guessable?
- Is `context.WithTimeout` used for all external calls?
- Are sessions cleaned up on disconnect (no memory leak)?
### Phase 6: Resource Leak Audit
```go
// File descriptors — check every os.Open has defer Close()
f, err := os.Open(path)
// missing defer f.Close() → leak
// Goroutine leaks — goroutines started without a stop mechanism
go func() {
for { select { case <-ch: ... } } // ← what closes ch?
}()
// DB rows — rows.Close() deferred after rows.Next() loop
rows, _ := db.Query(...)
defer rows.Close() // must be present
```
## Output Format
```
## Finding [N]: [Title] — [Critical/High/Medium/Low]
**File:** path/file.go:LINE
**Impact:** [what can go wrong]
**Root cause:** [exact code snippet]
**Fix:**
\`\`\`go
// corrected code
\`\`\`
```
Prioritize by: Critical (data loss/auth bypass) → High (crash/leak) → Medium (silent failure) → Low (hardening)
don't have the plugin yet? install it then click "run inline in claude" again.