Broken Function Level Authorization

Broken Function Level Authorization in Go (Gin) [CVE-2026-33222]

[Updated March 2026] Updated CVE-2026-33222

Overview

CVE-2026-33222 highlights a Broken Function Level Authorization flaw in NATS-Server where users with JetStream admin API access could restore a stream to a different stream name, effectively crossing resource boundaries and potentially exposing or corrupting data. Versions prior to 2.11.15 and 2.12.6 contained the issue, with fixes applied in those builds. This vulnerability illustrates how permission at the function level (e.g., an admin restore operation) can be insufficient if the operation is not scoped to a particular resource. In Go applications using Gin, a similar risk arises when privileged endpoints perform actions on resources without validating that the authenticated user is allowed to target the specific resource, enabling an attacker with broad privileges to affect resources they should not touch. If not mitigated, such flaws enable cross-resource manipulation, data leakage, or corruption even when a user is otherwise authenticated and authorized for high-privilege actions. In practical terms, a REST endpoint that accepts a restore request and uses a broad, global permission (like “restore” or “admin”) without verifying the target resource against the user’s allowed set can be abused. For example, a user could restore data into a stream they should not manage, overwriting protected data or gaining unintended access to other streams. The Go (Gin) remediation focuses on enforcing resource-level authorization: after confirming a user has the required high-level capability, the server must ensure the operation is permitted on the specified target resource (e.g., the target stream). This aligns with mitigating BFLA by scoping privileged actions to explicitly allowed resources, not just to roles. The fix is to implement per-resource checks, preferably with explicit RBAC policies or claims in tokens, and to reject requests where the target resource is not included in the caller’s allowed set. This approach mirrors the intent of the NATS fix, translating it into application-layer controls within Go (Gin) services: validate the target resource against the authenticated user’s permissions, and enforce least privilege across all privileged endpoints. Additionally, write tests that simulate attempts to operate on unauthorized resources and verify that such requests are rejected with proper HTTP status codes. Dynamic permission evaluation (not hard-coded resource lists) helps future-proof the app as resources evolve.

Affected Versions

NATS-Server < 2.11.15 and < 2.12.6

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "log"
  "net/http"
  "strings"

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

type User struct {
  Name            string
  Roles           []string
  AllowedStreams  []string
}

func hasRole(u User, role string) bool {
  for _, r := range u.Roles {
    if r == role {
      return true
    }
  }
  return false
}

func hasStreamAccess(u User, stream string) bool {
  for _, s := range u.AllowedStreams {
    if s == stream {
      return true
    }
  }
  return false
}

func extractUser(c *gin.Context) User {
  header := c.GetHeader("X-User")
  u := User{Name: "guest", Roles: []string{}, AllowedStreams: []string{}}
  if header == "" {
    return u
  }
  // Expected format: "name:alice|roles:restore,admin|streams:streamA,streamB"
  parts := strings.Split(header, "|")
  for _, p := range parts {
    kv := strings.SplitN(p, ":", 2)
    if len(kv) != 2 {
      continue
    }
    key := kv[0]
    val := kv[1]
    switch key {
    case "name":
      u.Name = val
    case "roles":
      if val != "" {
        u.Roles = strings.Split(val, ",")
      }
    case "streams":
      if val != "" {
        u.AllowedStreams = strings.Split(val, ",")
      }
    }
  }
  return u
}

type RestoreRequest struct {
  SourceStream string `json:"source"`
  TargetStream string `json:"target"`
}

func vulnerableRestoreHandler(c *gin.Context) {
  user := extractUser(c)
  if !hasRole(user, "restore") {
    c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: restore permission required"})
    return
  }
  var req RestoreRequest
  if err := c.ShouldBindJSON(&req); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
    return
  }
  // Vulnerable: no resource-level check on TargetStream; any target is allowed for users with the restore role
  c.JSON(http.StatusOK, gin.H{
    "status":   "restored (vulnerable)",
    "from":     req.SourceStream,
    "to":       req.TargetStream,
    "user":     user.Name,
  })
}

func fixedRestoreHandler(c *gin.Context) {
  user := extractUser(c)
  if !hasRole(user, "restore") {
    c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: restore permission required"})
    return
  }
  var req RestoreRequest
  if err := c.ShouldBindJSON(&req); err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
    return
  }
  if !hasStreamAccess(user, req.TargetStream) {
    c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: target stream not permitted for this user"})
    return
  }
  // Secure operation
  c.JSON(http.StatusOK, gin.H{
    "status": "restored",
    "from":   req.SourceStream,
    "to":     req.TargetStream,
    "user":   user.Name,
  })
}

func main() {
  r := gin.Default()
  r.POST("/v1/restore/vulnerable", vulnerableRestoreHandler)
  r.POST("/v1/restore/fix", fixedRestoreHandler)
  if err := r.Run(":8080"); err != nil {
    log.Fatal(err)
  }
}

CVE References

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