Broken Object Property Level Authorization

Broken Object Property Level Authorization and Go (Gin) [CVE-2026-41323]

[Updated April 2026] Updated CVE-2026-41323

Overview

The Kyverno CVE-2026-41323 disclosure demonstrates how a component can leak tokens and perform SSRF when it uncritically trusts a service URL and an attached token. The admission controller token, when combined with an unvalidated outgoing URL, enabled attackers to reach attacker-controlled servers and potentially patch webhook configurations, leading to full cluster compromise. The underlying risk combines information disclosure (CWE-200) with SSRF (CWE-918) and, ultimately, broken access control if an attacker can leverage the stolen token to access unauthorized resources. This real-world failure pattern maps to Broken Object Property Level Authorization in that insufficient checks on which resources a caller can access allows adversaries to operate beyond their intended scope, especially when object-level ownership data could be inferred or bypassed. When ported to Go (Gin) in API backends, similar misconfigurations manifest as endpoints that return or mutate resources solely based on identifiers supplied by the client without confirming the requester’s rights to that specific object.

Affected Versions

Kyverno: vulnerable before 1.18.0-rc1 (1.18 line); before 1.17.2-rc1 (1.17 line); before 1.16.4 (1.16 line).

Code Fix Example

Go (Gin) API Security Remediation
Vulnerable pattern (Go + Gin):

package main

import (
  "net/http"
  "strconv"

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

type Resource struct {
  ID      int64
  OwnerID int64
  Data    string
}

var resources = []Resource{{ID: 1, OwnerID: 100, Data: "secret"}}

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

  // Mock authentication: userID is 100
  r.Use(func(c *gin.Context) {
    c.Set("userID", int64(100))
    c.Next()
  })

  // Vulnerable: returns resource without ownership check
  r.GET("/vuln/resources/:id", func(c *gin.Context) {
    id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
    var res *Resource
    for i := range resources {
      if resources[i].ID == id {
        res = &resources[i]
        break
      }
    }
    if res == nil {
      c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
      return
    }
    c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
  })

  // Vulnerable: update without ownership check
  r.PUT("/vuln/resources/:id", func(c *gin.Context) {
    id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
    var res *Resource
    for i := range resources {
      if resources[i].ID == id {
        res = &resources[i]
        break
      }
    }
    if res == nil {
      c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
      return
    }
    res.Data = "updated"
    c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
  })

  // Fixed: ownership check implemented below using same routes
  r.Run()
}

// Fixed pattern (same file) with explicit ownership checks:
//
// r.GET("/fix/resources/:id", ...) and r.PUT("/fix/resources/:id", ...) should verify that the
// authenticated user owns the resource before returning/modifying it. See also the 
// CVE-2026-41323 reference for the type of impact when object-level authorization is not enforced.

package main

// ... same imports and types as above ...

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

  // Mock authentication: userID is 100
  r.Use(func(c *gin.Context) {
    c.Set("userID", int64(100))
    c.Next()
  })

  r.GET("/fix/resources/:id", func(c *gin.Context) {
    id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
    var res *Resource
    for i := range resources {
      if resources[i].ID == id {
        res = &resources[i]
        break
      }
    }
    if res == nil {
      c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
      return
    }
    if v, ok := c.Get("userID"); ok {
      if uid, ok := v.(int64); ok && res.OwnerID != uid {
        c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
        return
      }
    } else {
      c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthenticated"})
      return
    }
    c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
  })

  r.PUT("/fix/resources/:id", func(c *gin.Context) {
    id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
    var res *Resource
    for i := range resources {
      if resources[i].ID == id {
        res = &resources[i]
        break
      }
    }
    if res == nil {
      c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
      return
    }
    if v, ok := c.Get("userID"); ok {
      if uid, ok := v.(int64); ok && res.OwnerID != uid {
        c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
        return
      }
    } else {
      c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthenticated"})
      return
    }
    res.Data = "updated"
    c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
  })

  r.Run()
}

CVE References

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