| name | sc-lang-go |
| description | Go-specific security deep scan |
| license | MIT |
| metadata | {"author":"ersinkoc","category":"security","version":"1.0.0"} |
SC: Go Security Deep Scan
Purpose
Detects Go-specific security anti-patterns, common mistakes, and language-idiomatic attack vectors that generic vulnerability skills cannot catch.
Activation
Activates when Go is detected in architecture.md.
Checklist Reference
References references/go-security-checklist.md.
Go-Specific Vulnerability Patterns
1. Unsafe Package Usage and Reflect Abuse
Pattern: Direct use of unsafe.Pointer to bypass Go type system, or reflect to access unexported fields, disabling memory safety guarantees.
Dangerous functions/patterns:
unsafe.Pointer, unsafe.Sizeof, unsafe.Offsetof, unsafe.Alignof
reflect.NewAt, reflect.Value.Pointer(), reflect.Value.UnsafeAddr()
- Casting
unsafe.Pointer to uintptr and back (pointer arithmetic)
- Using
//go:linkname to access internal runtime symbols
Safe alternative: Use type-safe interfaces, generics (Go 1.18+), and exported APIs. If unsafe is unavoidable, isolate it in a single package with extensive tests and document the invariants.
Vulnerable code:
func readPrivateField(s interface{}) string {
v := reflect.ValueOf(s).Elem()
f := v.FieldByName("secret")
ptr := unsafe.Pointer(f.UnsafeAddr())
return *(*string)(ptr)
}
Safe code:
type Config struct {
Secret string
}
func (c *Config) GetSecret() string {
return c.Secret
}
2. CGo Boundary Safety
Pattern: Passing Go-managed memory across the CGo boundary without ensuring it remains valid, or accepting unsanitized C strings that may not be null-terminated.
Dangerous functions/patterns:
C.CString() without corresponding C.free()
- Passing Go pointers to C functions that store them (violates CGo pointer rules)
C.GoString() on untrusted C memory without length bounds
C.GoBytes() with attacker-controlled length parameter
Safe alternative: Always free C-allocated memory, use C.GoStringN() with validated lengths, and never let C code retain Go pointers across calls.
Vulnerable code:
import "C"
func processInput(input string) {
cStr := C.CString(input)
C.some_c_function(cStr)
}
Safe code:
import "C"
import "unsafe"
func processInput(input string) {
cStr := C.CString(input)
defer C.free(unsafe.Pointer(cStr))
C.some_c_function(cStr)
}
3. Goroutine Leaks and Channel Deadlocks
Pattern: Spawning goroutines that block forever on channel operations, HTTP requests, or I/O without cancellation, leading to memory exhaustion and denial of service.
Dangerous functions/patterns:
- Unbuffered channel sends/receives without select + context
go func() without a termination path
- Channel created but never closed, leaving receivers blocked
- Infinite loops inside goroutines without exit conditions
Safe alternative: Always pass a context.Context to goroutines, use select with ctx.Done(), and prefer buffered channels or explicit close signals.
Vulnerable code:
func fetch(urls []string) []string {
results := make(chan string)
for _, url := range urls {
go func(u string) {
resp, _ := http.Get(u)
body, _ := io.ReadAll(resp.Body)
results <- string(body)
}(url)
}
return []string{<-results}
}
Safe code:
func fetch(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return err
}
results[i] = string(body)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
4. Race Conditions (go vet -race)
Pattern: Concurrent reads and writes to shared variables without synchronization, leading to data corruption and undefined behavior.
Dangerous functions/patterns:
- Global mutable state accessed from multiple goroutines
- Map read/write from different goroutines (runtime panic in Go 1.6+)
- Struct fields modified concurrently without mutex
- Non-atomic counter increments (
count++ from multiple goroutines)
Safe alternative: Use sync.Mutex, sync.RWMutex, sync/atomic, or sync.Map. Run CI with -race flag. Prefer channel-based communication where possible.
Vulnerable code:
var cache = make(map[string]string)
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if v, ok := cache[key]; ok {
fmt.Fprint(w, v)
return
}
cache[key] = computeValue(key)
}
Safe code:
var (
cache = make(map[string]string)
cacheMu sync.RWMutex
)
func handler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
cacheMu.RLock()
v, ok := cache[key]
cacheMu.RUnlock()
if ok {
fmt.Fprint(w, v)
return
}
value := computeValue(key)
cacheMu.Lock()
cache[key] = value
cacheMu.Unlock()
fmt.Fprint(w, value)
}
5. html/template vs text/template XSS
Pattern: Using text/template to render HTML content, which performs no escaping and allows script injection.
Dangerous functions/patterns:
text/template.New() used for HTML output
template.HTML() type cast to bypass escaping in html/template
template.JS(), template.CSS(), template.URL() with user input
- Rendering user input in
<script> blocks even with html/template
Safe alternative: Always use html/template for any output rendered in a browser. Never cast user-controlled data to template.HTML. Use CSP headers as defense in depth.
Vulnerable code:
import "text/template"
func renderPage(w http.ResponseWriter, username string) {
t := template.Must(template.New("page").Parse(
`<h1>Welcome, {{.}}</h1>`))
t.Execute(w, username)
}
Safe code:
import "html/template"
func renderPage(w http.ResponseWriter, username string) {
t := template.Must(template.New("page").Parse(
`<h1>Welcome, {{.}}</h1>`))
t.Execute(w, username)
}
6. crypto/rand vs math/rand
Pattern: Using math/rand for security-sensitive operations such as token generation, password creation, or nonce selection.
Dangerous functions/patterns:
math/rand.Read(), math/rand.Int(), math/rand.Intn()
math/rand.New(math/rand.NewSource(time.Now().UnixNano())) for tokens
- Any
math/rand use for session IDs, CSRF tokens, OTPs, or salts
Safe alternative: Use crypto/rand.Read() or crypto/rand.Int() for all security-sensitive randomness. In Go 1.22+, math/rand/v2 auto-seeds from crypto/rand but still should not be used for cryptographic purposes.
Vulnerable code:
import "math/rand"
func generateToken() string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 32)
for i := range b {
b[i] = charset[rand.Intn(len(charset))]
}
return string(b)
}
Safe code:
import "crypto/rand"
import "encoding/hex"
func generateToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
7. TLS Configuration Mistakes
Pattern: Disabling TLS verification, using deprecated TLS versions, or configuring weak cipher suites in production code.
Dangerous functions/patterns:
InsecureSkipVerify: true in tls.Config
MinVersion set to tls.VersionTLS10 or tls.VersionTLS11
- Explicit weak cipher suites in
CipherSuites list
- Custom
VerifyPeerCertificate that always returns nil
http.DefaultTransport modification with insecure TLS
Safe alternative: Let Go select cipher suites automatically (secure defaults since Go 1.17). Set MinVersion: tls.VersionTLS12. Never set InsecureSkipVerify in production.
Vulnerable code:
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
MinVersion: tls.VersionTLS10,
},
},
}
Safe code:
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
},
}
8. os/exec Command Injection
Pattern: Building shell commands from user input using string concatenation or passing user input to shell interpreters.
Dangerous functions/patterns:
exec.Command("sh", "-c", userInput)
exec.Command("bash", "-c", fmt.Sprintf("grep %s file", userInput))
- String concatenation in command arguments
os.StartProcess with unsanitized arguments
Safe alternative: Pass arguments as separate elements to exec.Command (bypasses shell interpretation). Validate input against an allowlist. Never invoke a shell interpreter with user-controlled strings.
Vulnerable code:
func search(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
cmd := exec.Command("sh", "-c", "grep "+query+" /var/log/app.log")
output, _ := cmd.CombinedOutput()
w.Write(output)
}
Safe code:
func search(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
cmd := exec.Command("grep", "--", query, "/var/log/app.log")
output, err := cmd.CombinedOutput()
if err != nil {
http.Error(w, "search failed", http.StatusInternalServerError)
return
}
w.Write(output)
}
9. filepath.Join Traversal
Pattern: Assuming filepath.Join prevents directory traversal. It resolves .. components but does not reject them, allowing escape from the intended base directory.
Dangerous functions/patterns:
filepath.Join(baseDir, userInput) without prefix validation
path.Join (URL path variant) with unchecked input
- Serving files with
http.ServeFile using unvalidated paths
- Using
os.Open(filepath.Join(base, userInput)) without checking result is under base
Safe alternative: After joining, verify the result starts with the intended base directory using filepath.Rel or strings.HasPrefix on the cleaned absolute path.
Vulnerable code:
func serveFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("file")
path := filepath.Join("/var/www/static", name)
http.ServeFile(w, r, path)
}
Safe code:
func serveFile(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("file")
basePath := "/var/www/static"
fullPath := filepath.Join(basePath, filepath.Clean("/"+name))
if !strings.HasPrefix(fullPath, basePath+string(os.PathSeparator)) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
http.ServeFile(w, r, fullPath)
}
10. net/http Missing Timeouts and Header Injection
Pattern: Using default http.Server or http.Client without timeouts, enabling slowloris attacks and resource exhaustion. Also, injecting CRLF into HTTP headers via user input.
Dangerous functions/patterns:
http.ListenAndServe() (no timeouts set)
&http.Server{} without ReadTimeout, WriteTimeout, IdleTimeout
http.DefaultClient (zero timeout = wait forever)
w.Header().Set("Location", userInput) (CRLF injection in Go < 1.22)
http.Get(userURL) without timeout or SSRF protection
Safe alternative: Always configure explicit timeouts on both servers and clients. Validate and sanitize header values. Use net/http built-in header sanitization (Go 1.22+).
Vulnerable code:
func main() {
http.ListenAndServe(":8080", handler)
}
Safe code:
func main() {
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ReadHeaderTimeout: 2 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(srv.ListenAndServe())
}
11. encoding/json Deserialization Risks
Pattern: Unmarshaling JSON from untrusted sources without size limits, struct validation, or awareness of Go zero-value behavior for missing fields.
Dangerous functions/patterns:
json.NewDecoder(r.Body).Decode(&obj) without http.MaxBytesReader
- Missing field validation after unmarshal (zero values treated as valid)
json.Number misuse leading to integer overflow
json.RawMessage stored and re-serialized without validation
interface{} as target type allowing type confusion
Safe alternative: Limit body size with http.MaxBytesReader. Use DisallowUnknownFields(). Validate all fields post-unmarshal. Use strongly typed structs.
Vulnerable code:
func createUser(w http.ResponseWriter, r *http.Request) {
var user User
json.NewDecoder(r.Body).Decode(&user)
db.Create(&user)
}
Safe code:
func createUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
var user User
if err := dec.Decode(&user); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := validateUser(user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
db.Create(&user)
}
12. Integer Overflow in Type Conversions
Pattern: Converting between integer types (especially int64 to int32, or int to uint) without bounds checking, leading to silent truncation or sign flip.
Dangerous functions/patterns:
int32(someInt64) without range check
uint(signedInt) when signedInt can be negative
int(someUint64) when value exceeds math.MaxInt
strconv.Atoi result used directly as array index or allocation size
- Arithmetic on
int values near math.MaxInt without overflow checks
Safe alternative: Always validate ranges before narrowing conversions. Use math.MaxInt32, math.MinInt32, etc., for bounds checks. Consider using math/big for arbitrary precision.
Vulnerable code:
func allocateBuffer(r *http.Request) []byte {
sizeStr := r.URL.Query().Get("size")
size, _ := strconv.ParseInt(sizeStr, 10, 64)
buf := make([]byte, int32(size))
return buf
}
Safe code:
func allocateBuffer(r *http.Request) ([]byte, error) {
sizeStr := r.URL.Query().Get("size")
size, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil || size <= 0 || size > 10*1024*1024 {
return nil, fmt.Errorf("invalid size: must be 1 to 10MB")
}
return make([]byte, size), nil
}
13. Go Module Supply Chain
Pattern: Pulling in unvetted dependencies, using replace directives pointing to mutable sources, or ignoring checksum database verification.
Dangerous functions/patterns:
GONOSUMCHECK=* or GONOSUMDB=* environment variables
GOFLAGS=-insecure allowing HTTP module downloads
replace directives in go.mod pointing to remote repos (not local paths)
- Importing vanity import paths that redirect to untrusted repositories
- Dependencies with
+incompatible suffix (pre-module, no verified checksums)
- Missing
go.sum from version control
Safe alternative: Commit go.sum to version control. Use GOPRIV only for genuinely private modules. Run go mod verify in CI. Use govulncheck to scan for known vulnerabilities. Pin dependencies to exact versions.
Vulnerable code:
// go.mod with mutable replace directive
module myapp
go 1.21
require github.com/untrusted/lib v1.0.0
replace github.com/untrusted/lib => github.com/someone-else/fork master
// ^^^ master is mutable, could change at any time
Safe code:
// go.mod with pinned, checksummed dependencies
module myapp
go 1.21
require github.com/trusted/lib v1.2.3
// go.sum committed, GONOSUMCHECK not set, govulncheck in CI
14. context.Context Misuse
Pattern: Ignoring context cancellation in long-running operations, storing security-sensitive values in context, or creating contexts that never get cancelled.
Dangerous functions/patterns:
context.Background() in request handlers instead of r.Context()
context.TODO() left in production code
- Storing authentication tokens in context values (no type safety)
- Ignoring
ctx.Done() in loops or blocking operations
context.WithCancel without ever calling the cancel function (leak)
Safe alternative: Always propagate the request context. Use typed context keys. Always defer cancel functions. Check ctx.Err() in loops.
Vulnerable code:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
result, err := expensiveQuery(ctx)
}
ctx = context.WithValue(ctx, "authToken", token)
Safe code:
type contextKey int
const authTokenKey contextKey = iota
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := expensiveQuery(ctx)
if err != nil {
if ctx.Err() != nil {
return
}
http.Error(w, "query failed", http.StatusInternalServerError)
return
}
}
ctx = context.WithValue(ctx, authTokenKey, token)
15. Defer Ordering Bugs
Pattern: Misunderstanding LIFO execution order of defer statements, or deferring in loops causing resource accumulation, or deferring calls with evaluated-at-defer-time arguments.
Dangerous functions/patterns:
defer f.Close() before checking os.Open error (nil pointer panic)
defer inside a loop (resources not released until function returns)
defer resp.Body.Close() without checking if resp is nil
defer mu.Unlock() placed before mu.Lock() (unlock before lock)
- Deferred function with arguments evaluated at defer time, not execution time
Safe alternative: Always check errors before deferring cleanup. Use anonymous functions for deferred calls needing current variable values. Extract loop bodies into separate functions when deferring inside loops.
Vulnerable code:
func processFiles(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close()
process(f)
}
return nil
}
Safe code:
func processFiles(paths []string) error {
for _, p := range paths {
if err := processOneFile(p); err != nil {
return err
}
}
return nil
}
func processOneFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
return process(f)
}
16. Panic Recovery Anti-Patterns
Pattern: Using recover() to silently swallow panics, hiding security-critical errors, or failing to recover in goroutines (crashing the entire process).
Dangerous functions/patterns:
recover() that discards the panic value without logging
- Missing
recover() in spawned goroutines (unrecovered panic kills process)
- Using
panic for control flow or error handling
- Recovery that continues execution in a corrupted state
- Panic values containing sensitive information exposed to users
Safe alternative: Always log recovered panics with stack traces. Return 500 errors to clients without internal details. Use middleware for HTTP panic recovery. Never continue business logic after recovery.
Vulnerable code:
func handler(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
}
}()
riskyOperation()
}
Safe code:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", rec, stack)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
17. gRPC Security
Pattern: Running gRPC services without TLS, missing authentication interceptors, or trusting client-provided metadata without validation.
Dangerous functions/patterns:
grpc.NewServer() without TLS credentials (plaintext gRPC)
- Missing unary/stream interceptors for authentication
metadata.FromIncomingContext() values used without validation
grpc.WithInsecure() (deprecated) or grpc.WithTransportCredentials(insecure.NewCredentials())
- No rate limiting on streaming RPCs
- Unbounded message sizes (
grpc.MaxRecvMsgSize set too high)
Safe alternative: Always use TLS. Implement auth interceptors. Validate all metadata values. Set reasonable message size limits. Use deadlines on all RPCs.
Vulnerable code:
func main() {
srv := grpc.NewServer()
pb.RegisterMyServiceServer(srv, &myService{})
lis, _ := net.Listen("tcp", ":50051")
srv.Serve(lis)
}
Safe code:
func main() {
creds, err := credentials.NewServerTLSFromFile("cert.pem", "key.pem")
if err != nil {
log.Fatal(err)
}
srv := grpc.NewServer(
grpc.Creds(creds),
grpc.UnaryInterceptor(authUnaryInterceptor),
grpc.StreamInterceptor(authStreamInterceptor),
grpc.MaxRecvMsgSize(4 * 1024 * 1024),
)
pb.RegisterMyServiceServer(srv, &myService{})
lis, _ := net.Listen("tcp", ":50051")
log.Fatal(srv.Serve(lis))
}
18. sql.DB Pool Exhaustion
Pattern: Failing to close sql.Rows, holding transactions open indefinitely, or misconfiguring connection pool settings, leading to connection starvation and denial of service.
Dangerous functions/patterns:
db.Query() without defer rows.Close()
db.Begin() without guaranteed tx.Rollback() or tx.Commit()
- No
SetMaxOpenConns, SetMaxIdleConns, or SetConnMaxLifetime
- Scanning rows inside a transaction that holds a lock
QueryRow().Scan() error ignored (connection may not be returned)
Safe alternative: Always close rows with defer. Set pool limits. Use context-aware queries. Ensure transactions are always committed or rolled back.
Vulnerable code:
func getUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name FROM users")
if err != nil {
return nil, err
}
var users []User
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name)
users = append(users, u)
}
return users, nil
}
Safe code:
func getUsers(ctx context.Context, db *sql.DB) ([]User, error) {
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name); err != nil {
return nil, fmt.Errorf("scanning user: %w", err)
}
users = append(users, u)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterating rows: %w", err)
}
return users, nil
}
func initDB() *sql.DB {
db, err := sql.Open("postgres", dsn)
if err != nil {
log.Fatal(err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
return db
}
19. Slice/Map Concurrent Access
Pattern: Reading and writing to slices or maps from multiple goroutines without synchronization. Maps cause fatal runtime panics; slices cause silent data corruption.
Dangerous functions/patterns:
- Appending to a shared slice from multiple goroutines
- Reading/writing shared map without mutex (fatal: concurrent map writes)
- Passing a slice to a goroutine and modifying the original
- Sharing slice headers across goroutines (data races on len/cap)
Safe alternative: Use sync.Map for concurrent map access, or protect maps/slices with sync.RWMutex. Prefer goroutine-local data and channels. Use the -race detector in tests.
Vulnerable code:
var results []string
func collectResults(urls []string) []string {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
result := fetch(u)
results = append(results, result)
}(url)
}
wg.Wait()
return results
}
Safe code:
func collectResults(urls []string) []string {
var (
mu sync.Mutex
results []string
wg sync.WaitGroup
)
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
result := fetch(u)
mu.Lock()
results = append(results, result)
mu.Unlock()
}(url)
}
wg.Wait()
return results
}
20. Error Wrapping Info Disclosure
Pattern: Returning internal error details (file paths, SQL queries, stack traces, internal hostnames) to external users through error wrapping chains or raw error messages in HTTP responses.
Dangerous functions/patterns:
http.Error(w, err.Error(), 500) where err contains internal details
fmt.Errorf("query %s failed: %w", sqlQuery, err) exposed to API consumer
- Logging frameworks that serialize the full error chain to response bodies
errors.Unwrap chains exposing driver-level errors to clients
- Panic messages containing file paths reaching the user
Safe alternative: Map internal errors to generic user-facing messages. Log detailed errors server-side only. Use error sentinel types to classify errors without exposing details.
Vulnerable code:
func getUser(w http.ResponseWriter, r *http.Request) {
user, err := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
if err != nil {
http.Error(w, fmt.Sprintf("failed to get user: %v", err), 500)
return
}
}
Safe code:
func getUser(w http.ResponseWriter, r *http.Request) {
user, err := db.QueryRowContext(r.Context(),
"SELECT * FROM users WHERE id = $1", id)
if err != nil {
log.Printf("getUser: query failed for id=%s: %v", id, err)
http.Error(w, "user lookup failed", http.StatusInternalServerError)
return
}
}
Scan Procedure
- Identify Go source files: Locate all
.go files, go.mod, and go.sum in the target.
- Dependency analysis: Check
go.mod for risky dependencies, replace directives, and missing checksums.
- Run static checks: Verify
go vet, staticcheck, and govulncheck are configured in CI.
- Pattern scan: For each category above, search the codebase for dangerous patterns using AST-aware grep or
go vet analysis.
- Context evaluation: Determine if each finding is exploitable given the application threat model (internet-facing vs internal, data sensitivity).
- Severity assignment: Rate each finding using CVSS or qualitative severity (CRITICAL / HIGH / MEDIUM / LOW / INFO).
Output Format
Each finding MUST be reported in the following standard format:
### [SEVERITY] Finding Title
- **Category:** (one of the 20 categories above)
- **Location:** `package/file.go:line`
- **Pattern Matched:** (the dangerous pattern detected)
- **Description:** Concise explanation of the vulnerability and its impact.
- **Exploitability:** How an attacker would exploit this in practice.
- **Remediation:** Specific code change or configuration fix required.
- **Reference:** Link to CWE, Go documentation, or security advisory.
Findings should be grouped by severity (CRITICAL first, then HIGH, MEDIUM, LOW, INFO) and deduplicated. Each finding must reference exactly one category from the checklist above.