Unrestricted Resource Consumption

Unrestricted Resource Consumption in Go (Gin) [CVE-2026-35441]

[Fixed month year] Updated CVE-2026-35441

Overview

Unrestricted resource consumption vulnerabilities occur when a service processes inputs that cause an explosion of internal work, such as repeatedly invoking expensive DB queries. CVE-2026-35441 demonstrates how GraphQL aliasing can cause a single request to trigger many expensive relational queries concurrently, overwhelming CPU, memory, and I/O. In practice, an authenticated user could abuse GraphQL aliases to drive a large number of queries without built-in throttling, leading to denial of service or degraded service for other users. The Directus bug was fixed in 11.17.0, illustrating the need for per-request cost controls and safe resolver design when GraphQL is exposed. In Go applications using Gin, a similar pattern can manifest if GraphQL resolvers fire independent queries per alias and there is no deduplication, batching, or cost-limiting, enabling resource exhaustion from a single malicious request.

Affected Versions

Directus: pre-11.17.0 (fixed in 11.17.0); Go Gin-specific versions not applicable; CVE demonstrates broader pattern

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "log"
  "net/http"
  "sync"
  "time"

  "github.com/gin-gonic/gin"
)

type User struct {
  ID   string `json:"id"`
  Name string `json:"name"`
}

// Mock DB
var mockDB = map[string]User{
  "alice": {ID: "alice", Name: "Alice"},
  "bob":   {ID: "bob", Name: "Bob"},
  "carol": {ID: "carol", Name: "Carol"},
}

// VULNERABLE: launches a goroutine per alias without bounds
func vulnerableResolve(ids []string) map[string]User {
  results := make(map[string]User, len(ids))
  var mu sync.Mutex
  var wg sync.WaitGroup
  for _, id := range ids {
    wg.Add(1)
    go func(id string) {
      defer wg.Done()
      // Simulate expensive per-alias DB call
      time.Sleep(30 * time.Millisecond)
      mu.Lock()
      if u, ok := mockDB[id]; ok {
        results[id] = u
      } else {
        results[id] = User{ID: id, Name: ""}
      }
      mu.Unlock()
    }(id)
  }
  wg.Wait()
  return results
}

// FIX: batch fetches, deduplicates, and uses a single (simulated) batch DB call
func fixedResolve(ids []string) map[string]User {
  // Deduplicate IDs
  uniq := make(map[string]struct{}, len(ids))
  for _, id := range ids {
    uniq[id] = struct{}{}
  }
  // Batch fetch (single simulated DB call)
  time.Sleep(80 * time.Millisecond)
  res := make(map[string]User, len(uniq))
  for id := range uniq {
    if u, ok := mockDB[id]; ok {
      res[id] = u
    } else {
      res[id] = User{ID: id, Name: ""}
    }
  }
  return res
}

type GraphQLRequest struct {
  IDs []string `json:"ids"`
}

func main() {
  r := gin.Default()

  // Vulnerable endpoint: demonstrates unbounded per-alias concurrency
  r.POST("/graphql/vuln", func(c *gin.Context) {
    var req GraphQLRequest
    if err := c.BindJSON(&req); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
      return
    }
    res := vulnerableResolve(req.IDs)
    c.JSON(http.StatusOK, res)
  })

  // Fixed endpoint: demonstrates batched, deduplicated resolution
  r.POST("/graphql/fix", func(c *gin.Context) {
    var req GraphQLRequest
    if err := c.BindJSON(&req); err != nil {
      c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
      return
    }
    res := fixedResolve(req.IDs)
    c.JSON(http.StatusOK, res)
  })

  log.Println("Server running on :8080")
  if err := r.Run(":8080"); err != nil {
    log.Fatal(err)
  }
}

CVE References

Choose which optional cookies to allow. You can change this any time.