Skip to content

rhamitarora/go-vulnerability-categories

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Code Vulnerability Categories

A reference guide for identifying silent, production-dangerous vulnerabilities in Go codebases. These issues are typically invisible in tests but cause resource exhaustion, data races, goroutine leaks, or security exposures in production.


Table of Contents

  1. Resource Leaks
  2. Goroutine Leaks
  3. Nil / Uninitialized Pointer Dereferences
  4. Concurrency / Race Conditions
  5. Error Handling Anti-Patterns
  6. Memory Leaks / Unbounded Growth
  7. Kubernetes / Controller-Runtime Specific
  8. Security-Class Vulnerabilities
  9. Timeout / Deadlock Risks

1. Resource Leaks

Resources that are opened but never explicitly closed cause file descriptor exhaustion, socket accumulation, and memory retention over time.

1.1 HTTP Client / Transport Not Closed

Severity: High
Symptom: Excessive open TCP connections, memory growth, goroutines not exiting.

rest.HTTPClientFor, &http.Transport{}, and &http.Client{} each hold an internal connection pool. If the client is not retained and CloseIdleConnections() is never called, idle connections accumulate per call-site invocation.

// BAD — httpClient goes out of scope; connections never drained
httpClient, err := rest.HTTPClientFor(config)
cli, _ := kubernetes.NewForConfigAndClient(config, httpClient)
return &MyStruct{cli: cli} // httpClient not stored, never closed

// GOOD — retain and close
type MyStruct struct {
    cli        kubernetes.Interface
    httpClient *http.Client
}

func (s *MyStruct) Close() {
    s.httpClient.CloseIdleConnections()
}

What to grep for:

rest.HTTPClientFor
&http.Transport{
&http.Client{

1.2 HTTP Response Body Not Closed

Severity: High
Symptom: Socket/file descriptor leak; connection cannot be reused.

Every http.Response.Body must be fully read and closed, even on error responses. Forgetting defer resp.Body.Close() prevents the underlying TCP connection from being returned to the pool.

// BAD
resp, err := client.Do(req)
if err != nil {
    return err
}
data, _ := io.ReadAll(resp.Body)

// GOOD
resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)

What to grep for:

client.Do(
http.Get(
http.Post(

Then verify each call site has defer resp.Body.Close().


1.3 io.ReadCloser Not Closed

Severity: Medium
Symptom: Open file handles or streaming connections never released.

Pod log streams, archive readers, and other io.ReadCloser values must be closed after use.

// BAD
logs, _ := podClient.GetLogs(podName, &opts).Stream(ctx)
scanner := bufio.NewScanner(logs)

// GOOD
logs, err := podClient.GetLogs(podName, &opts).Stream(ctx)
if err != nil {
    return err
}
defer logs.Close()

1.4 os.File Not Closed

Severity: Medium
Symptom: File descriptor exhaustion under high load.

// BAD
f, err := os.Open(path)
if err != nil {
    return err
}
data, _ := io.ReadAll(f)

// GOOD
f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

1.5 gRPC Connection Not Closed

Severity: High
Symptom: Open TCP connections and goroutines for keepalive/health checks.

// BAD
conn, _ := grpc.Dial(addr, opts...)
client := pb.NewMyServiceClient(conn)
// conn never closed

// GOOD
conn, err := grpc.Dial(addr, opts...)
if err != nil {
    return err
}
defer conn.Close()

1.6 time.Ticker / time.Timer Not Stopped

Severity: Medium
Symptom: Internal goroutine inside the ticker never exits; goroutine leak.

// BAD
ticker := time.NewTicker(5 * time.Second)
for range ticker.C {
    doWork()
}

// GOOD
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
    select {
    case <-ticker.C:
        doWork()
    case <-ctx.Done():
        return
    }
}

1.7 Database Rows Not Closed

Severity: Medium
Symptom: DB connection not returned to pool; query hangs under load.

// BAD
rows, _ := db.QueryContext(ctx, sql)
for rows.Next() { ... }

// GOOD
rows, err := db.QueryContext(ctx, sql)
if err != nil {
    return err
}
defer rows.Close()

1.8 context.WithCancel — Cancel Not Deferred

Severity: High
Symptom: Child context goroutine leaks; associated resources never freed.

// BAD
ctx, cancel := context.WithCancel(parentCtx)
doWork(ctx)
// cancel never called

// GOOD
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
doWork(ctx)

1.9 context.WithTimeout — Cancel Not Called

Severity: High
Symptom: Same as 1.8 — internal timer goroutine leaks until parent context is done.

// BAD
ctx, _ := context.WithTimeout(parent, 30*time.Second)
doWork(ctx)

// GOOD
ctx, cancel := context.WithTimeout(parent, 30*time.Second)
defer cancel()
doWork(ctx)

2. Goroutine Leaks

Goroutines that never exit consume memory and scheduler resources permanently. They are invisible until the process OOMs or the goroutine count metric spikes.

2.1 Goroutine Blocked on Unbuffered Channel Forever

Severity: High

If the receiver exits early (e.g., on error), any goroutine blocked on a send to an unbuffered channel will hang forever.

// BAD — if receiver returns early, this goroutine is stuck
ch := make(chan Result)
go func() {
    ch <- expensiveWork() // blocks forever if nobody reads
}()
result, ok := <-ch
if !ok {
    return // goroutine above is now leaked
}

// GOOD — use select with context
go func() {
    result := expensiveWork()
    select {
    case ch <- result:
    case <-ctx.Done():
    }
}()

2.2 Goroutine Ignores Context Cancellation

Severity: High

A goroutine that loops without checking ctx.Done() cannot be stopped externally.

// BAD
go func() {
    for {
        poll()
        time.Sleep(5 * time.Second)
    }
}()

// GOOD
go func() {
    for {
        select {
        case <-ctx.Done():
            return
        case <-time.After(5 * time.Second):
            poll()
        }
    }
}()

2.3 errgroup Goroutine Started After Context Cancelled

Severity: Medium

In an errgroup loop, if one goroutine fails early the groupCtx is cancelled. Starting a new g.Go(...) after that with groupCtx-dependent work wastes resources.


2.4 sync.WaitGroup Counter Goes Negative

Severity: High — panics in production

Calling wg.Done() more times than wg.Add() causes an immediate panic.

// BAD
var wg sync.WaitGroup
wg.Add(1)
go func() {
    defer wg.Done()
    defer wg.Done() // double Done → panic
}()

2.5 sync.WaitGroup Never Waited

Severity: Medium
Symptom: Silent data race — caller returns while goroutines are still writing.

// BAD
var wg sync.WaitGroup
wg.Add(1)
go func() { defer wg.Done(); writeResult() }()
return result // goroutine may not have run yet

// GOOD
wg.Wait()
return result

2.6 Goroutine Leak on Early Return

Severity: High

A function that spawns goroutines and then returns on error without cancelling them.

// BAD
func process(items []Item) error {
    for _, item := range items {
        go handle(item) // no way to stop these
    }
    if err := validate(); err != nil {
        return err // goroutines above are now leaked
    }
    return nil
}

// GOOD — use errgroup or pass a cancellable context
g, ctx := errgroup.WithContext(parentCtx)
for _, item := range items {
    item := item
    g.Go(func() error { return handle(ctx, item) })
}
return g.Wait()

3. Nil / Uninitialized Pointer Dereferences

3.1 Nil Map Write

Severity: High — panics in production

// BAD — var declares a nil map; writing panics
var m map[string]string
m["key"] = "value" // panic: assignment to entry in nil map

// GOOD
m := make(map[string]string)
m["key"] = "value"

3.2 Nil Interface Method Call

Severity: High — panics in production

A typed nil satisfies an interface non-nil check but panics on method call.

// BAD
var p *MyProcessor = nil
var i Processor = p  // i != nil, but calling i.Process() panics

// GOOD — check the concrete value
if p == nil {
    return errors.New("processor not initialized")
}

3.3 Nil Pointer in Struct Field

Severity: High

// BAD
type Manager struct {
    client *kubernetes.Clientset
}
m := &Manager{} // client is nil
m.client.Get(...)  // panic

// GOOD — always initialize or guard before use
if m.client == nil {
    return errors.New("kubernetes client not initialized")
}

3.4 Return Value Ignored After Error

Severity: Medium

// BAD — val is zero/nil when err != nil
val, _ := foo()
process(val) // may panic or silently corrupt state

// GOOD
val, err := foo()
if err != nil {
    return err
}
process(val)

4. Concurrency / Race Conditions

4.1 Loop Variable Capture in Goroutine

Severity: High
Applies to: Go versions before 1.22

All goroutines in the loop capture the same loop variable, which has moved to the last value by the time they run.

// BAD (Go < 1.22)
for _, v := range items {
    go func() {
        process(v) // all goroutines see the same v
    }()
}

// GOOD — shadow the variable
for _, v := range items {
    v := v // new variable per iteration
    go func() {
        process(v)
    }()
}

Note: Go 1.22+ fixes this by default, but existing code written before 1.22 still has the bug.


4.2 Shared Struct Mutated Without Mutex

Severity: High
Symptom: Data corruption, intermittent wrong values, go test -race failures.

// BAD
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ } // not safe for concurrent use

// GOOD
type Counter struct {
    mu sync.Mutex
    n  int
}
func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.n++
}

4.3 Regular map Used With Concurrent Access

Severity: High — panics in production

Go's built-in map is not safe for concurrent reads and writes. Use sync.Map or protect with a mutex.

// BAD
var cache = map[string]string{}
go func() { cache["k"] = "v" }()
go func() { _ = cache["k"] }() // concurrent read+write → panic

// GOOD
var cache sync.Map
go func() { cache.Store("k", "v") }()
go func() { cache.Load("k") }()

4.4 sync.Once Error Not Propagated

Severity: Medium

sync.Once runs the function exactly once. If the initialization fails, subsequent callers get the zero value with no error.

// BAD
var once sync.Once
var client *Client

once.Do(func() {
    client, _ = newClient() // error swallowed; client stays nil
})
use(client) // may be nil

// GOOD — use a dedicated sync primitive or return the error from once

4.5 Double-Checked Locking Without atomic

Severity: High

Reading a flag outside a lock is a data race even if the write is protected.

// BAD
if !initialized {     // data race: read without lock
    mu.Lock()
    if !initialized {
        init()
        initialized = true
    }
    mu.Unlock()
}

// GOOD
if atomic.LoadInt32(&initialized) == 0 {
    mu.Lock()
    if atomic.LoadInt32(&initialized) == 0 {
        init()
        atomic.StoreInt32(&initialized, 1)
    }
    mu.Unlock()
}

5. Error Handling Anti-Patterns

5.1 Close() Error Silently Ignored

Severity: Medium
Symptom: Undetected write failures (e.g., flushed buffers not written to disk).

// BAD
defer f.Close() // error from Close() is dropped

// GOOD (in non-deferred context)
if err := f.Close(); err != nil {
    return fmt.Errorf("closing file: %w", err)
}

5.2 Error Swallowed With _

Severity: High

// BAD
result, _ = doImportantThing() // failure is invisible

// GOOD
result, err := doImportantThing()
if err != nil {
    return fmt.Errorf("doImportantThing: %w", err)
}

5.3 Error From Goroutine Lost

Severity: High

// BAD
go func() {
    if err := work(); err != nil {
        log.Error(err) // logged but caller never knows
    }
}()

// GOOD — use errgroup or an error channel
g.Go(func() error { return work() })
if err := g.Wait(); err != nil {
    return err
}

5.4 Wrapping Nil Error Creates Non-Nil Error

Severity: Medium

// BAD
func wrap(err error) error {
    return fmt.Errorf("context: %w", err) // if err is nil, this returns non-nil!
}

// GOOD
func wrap(err error) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("context: %w", err)
}

5.5 errors.Is Broken by Double Wrapping

Severity: Medium

Wrapping the same sentinel error twice can break errors.Is chains in older Go versions.

// BAD
err = fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", ErrNotFound))
// errors.Is(err, ErrNotFound) may fail in some Go < 1.20 scenarios

// GOOD — wrap once, consistently
err = fmt.Errorf("outer: inner: %w", ErrNotFound)

6. Memory Leaks / Unbounded Growth

6.1 Global Cache With No Eviction

Severity: High
Symptom: Steady memory growth; OOM under long-running workloads.

// BAD
var cache = map[string]*BigObject{} // grows forever

// GOOD — use a cache with TTL/LRU eviction
cache := lru.New(1000) // bounded

6.2 Slice Append Retaining Backing Array

Severity: Medium

Sub-slicing a large buffer and storing the sub-slice keeps the entire original array in memory.

// BAD
data := readHugeFile()
header := data[:100] // keeps the full data array alive

// GOOD — copy if you only need a small piece
header := make([]byte, 100)
copy(header, data[:100])

6.3 string(bigByteSlice) in Hot Path

Severity: Medium
Symptom: Excessive allocations under load; GC pressure.

// BAD — allocates a new string on every call
if string(body) == expectedBody { ... }

// GOOD
if bytes.Equal(body, []byte(expectedBody)) { ... }

6.4 time.After in a Loop

Severity: High
Symptom: Each loop iteration creates a new timer that is never stopped until it fires.

// BAD — leaks a timer per iteration
for {
    select {
    case <-time.After(5 * time.Second): // new timer every iteration
        doWork()
    case <-ctx.Done():
        return
    }
}

// GOOD — reuse the ticker
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
    select {
    case <-ticker.C:
        doWork()
    case <-ctx.Done():
        return
    }
}

6.5 Unbounded Channel Buffer

Severity: Medium

// BAD — if consumer is slow, producer fills memory
ch := make(chan Event, userProvidedCount)

// GOOD — use a fixed, reasoned bound
ch := make(chan Event, 100)

7. Kubernetes / Controller-Runtime Specific

7.1 NewForConfig Per Request — Never Closed

Severity: High
Symptom: Each admin API request creates a new http.Transport connection pool.

// BAD — called inside an HTTP handler
func (f *frontend) handleAdminRequest(w http.ResponseWriter, r *http.Request) {
    cli, _ := kubernetes.NewForConfig(restConfig) // new transport each request
    cli.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
    // cli and its transport are never closed
}

// GOOD — share a long-lived client or close explicitly
httpClient, _ := rest.HTTPClientFor(restConfig)
defer httpClient.CloseIdleConnections()
cli, _ := kubernetes.NewForConfigAndClient(restConfig, httpClient)

7.2 Multiple Clients Not Sharing Transport

Severity: Medium
Symptom: N clients = N connection pools to the same API server.

// BAD — three separate transports to the same cluster
cli, _         := kubernetes.NewForConfig(restConfig)
configcli, _   := configclient.NewForConfig(restConfig)
operatorcli, _ := operatorclient.NewForConfig(restConfig)

// GOOD — one shared transport
httpClient, _ := rest.HTTPClientFor(restConfig)
cli, _         := kubernetes.NewForConfigAndClient(restConfig, httpClient)
configcli, _   := configclient.NewForConfigAndClient(restConfig, httpClient)
operatorcli, _ := operatorclient.NewForConfigAndClient(restConfig, httpClient)

7.3 Informer Cache Not Started

Severity: Medium
Symptom: List/Get via informer always returns empty results.

// BAD
factory := informers.NewSharedInformerFactory(cli, 0)
lister := factory.Core().V1().Pods().Lister()
// factory.Start() never called — lister always empty

// GOOD
factory.Start(stopCh)
factory.WaitForCacheSync(stopCh)

7.4 Controller-Runtime Client Used Before Cache Sync

Severity: Medium

// BAD
go mgr.Start(ctx)
list := &corev1.PodList{}
mgr.GetClient().List(ctx, list) // cache may not be synced yet

// GOOD
go mgr.Start(ctx)
if !mgr.GetCache().WaitForCacheSync(ctx) {
    return errors.New("cache failed to sync")
}
mgr.GetClient().List(ctx, list)

7.5 Watch Stream Not Closed

Severity: High
Symptom: Background goroutine in the watch loop never exits.

// BAD
watcher, _ := cli.CoreV1().Pods("").Watch(ctx, metav1.ListOptions{})
for event := range watcher.ResultChan() { ... }
// watcher.Stop() never called

// GOOD
watcher, _ := cli.CoreV1().Pods("").Watch(ctx, metav1.ListOptions{})
defer watcher.Stop()
for event := range watcher.ResultChan() { ... }

8. Security-Class Vulnerabilities

8.1 TLS InsecureSkipVerify: true

Severity: Critical
Symptom: MitM attacks possible; certificate validation bypassed.

// BAD
tlsConfig := &tls.Config{InsecureSkipVerify: true}

// GOOD — always validate certificates in production
tlsConfig := &tls.Config{
    MinVersion: tls.VersionTLS12,
    RootCAs:    certPool,
}

8.2 Secret / Token in Log Output

Severity: Critical

// BAD
log.Infof("using token: %s", token)
log.Debugf("restConfig: %+v", restConfig) // may include bearer token

// GOOD — never log credentials
log.Infof("token configured (length: %d)", len(token))

8.3 exec.Command With Unsanitized Input

Severity: Critical — command injection

// BAD
exec.Command("bash", "-c", userInput).Run()

// GOOD — pass arguments as separate strings, never through a shell
exec.Command("myprogram", "--flag", sanitizedArg).Run()

8.4 math/rand for Security-Sensitive Values

Severity: High

// BAD — predictable output
token := strconv.Itoa(rand.Int())

// GOOD
b := make([]byte, 32)
_, err := crypto_rand.Read(b)
token := hex.EncodeToString(b)

8.5 SSRF via User-Provided URL

Severity: High
Context: An unvalidated URL from user input could reach Azure IMDS (XXX.254.XXX.254).

// BAD
url := r.URL.Query().Get("target")
http.Get(url) // attacker can reach internal Azure metadata endpoints

// GOOD — validate scheme, host, and port against an allowlist
if !isAllowedURL(url) {
    return api.NewCloudError(http.StatusBadRequest, ...)
}

8.6 //nolint / // #nosec Suppressing Real Issues

Severity: Medium

Suppression comments that mask real security findings should always have an explanatory comment.

// BAD
x := rand.Int() // #nosec

// GOOD
// Safe: this value is used only for non-security load balancing, not for tokens.
x := rand.Int() // #nosec G404

9. Timeout / Deadlock Risks

9.1 HTTP Client With No Timeout

Severity: High
Symptom: Process hangs indefinitely on unresponsive external service.

// BAD
client := &http.Client{} // no timeout

// GOOD
client := &http.Client{Timeout: 30 * time.Second}

9.2 Context Accepted But Not Propagated

Severity: Medium
Symptom: Cancellation is accepted by the function signature but has no effect.

// BAD
func fetchData(ctx context.Context) error {
    rows, err := db.Query(sql) // context not passed — cannot be cancelled
    ...
}

// GOOD
func fetchData(ctx context.Context) error {
    rows, err := db.QueryContext(ctx, sql)
    ...
}

9.3 Mutex Lock Without defer Unlock

Severity: High — deadlock on early return

// BAD
mu.Lock()
if err := validate(); err != nil {
    return err // mu is never unlocked → deadlock
}
mu.Unlock()

// GOOD
mu.Lock()
defer mu.Unlock()
if err := validate(); err != nil {
    return err
}

9.4 Channel Send With No Context Guard

Severity: High
Symptom: Sender blocks forever if receiver has already exited.

// BAD
resultCh <- value // blocks forever if receiver is gone

// GOOD
select {
case resultCh <- value:
case <-ctx.Done():
    return ctx.Err()
}

9.5 wait.PollUntil With Non-Cancellable Context

Severity: Medium

// BAD
wait.PollImmediateUntil(interval, conditionFunc, neverCancellingCtx.Done())
// if conditionFunc never returns true, this polls forever

// GOOD
timeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
wait.PollImmediateUntil(interval, conditionFunc, timeoutCtx.Done())

About

Go Code Vulnerability Categories

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages