Go Security Audit

MCP Tools

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.

Install

openclaw skills install go-security-audit

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:

// 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:

// 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:

// 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:

// 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

// 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)