Broken Object Property Level Authorization

Broken Object Property Level Authorization in Go (Gin) [CVE-2025-14726]

[Updated May 2026] Updated CVE-2025-14726

Overview

Broken Object Property Level Authorization (also known as per-object access control) can allow a user to access or modify resources they do not own by simply changing an identifier in the request. In real-world Go applications using Gin, this often leads to data leakage, unauthorized updates, or privilege escalation across tenants when an endpoint returns a resource by ID without verifying ownership. The impact ranges from leaking sensitive user data to violating regulatory requirements and eroding trust in the application. In Gin, handlers frequently pull a resource by ID from a URL or JSON payload and then return it without an explicit ownership check. If the database query only filters by the resource ID, or if the authenticated user context is ignored for the object, an attacker can slide through the ID space and access or manipulate other users' resources. This class of vulnerability is common when authorization is implemented at a coarse level (route or role) rather than at the per-object boundary. Detection and remediation focus on enforcing per-object access at the data layer, adding proper ownership checks, and testing with adversarial requests. Ensure every operation that targets a specific resource includes a constraint such as owner_id = current_user.id or an ACL check, use query scoping to limit visible records, and consider middleware to load and verify resources before business logic runs. Implement thorough unit and integration tests that simulate unauthorized access patterns and verify appropriate responses (403/404) and proper logging.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "net/http"
  "strconv"
  "github.com/gin-gonic/gin"
)

type User struct {
  ID int64
  Name string
}

type Resource struct {
  ID int64
  OwnerID int64
  Data string
}

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

func getResourceByID(id int64) *Resource {
  for i := range resources {
    if resources[i].ID == id {
      return &resources[i]
    }
  }
  return nil
}

func main() {
  r := gin.Default()
  r.GET("/vuln/resource/:id", vulnerableHandler)
  r.GET("/fix/resource/:id", fixedHandler)
  r.Run(":8080")
}

func getCurrentUser(_ *gin.Context) *User {
  // In real app, extract user from session/JWT
  return &User{ID: 100, Name: "Alice"}
}

func vulnerableHandler(c *gin.Context) {
  _ = getCurrentUser(c)
  id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  res := getResourceByID(id)
  if res == nil {
    c.Status(http.StatusNotFound)
    return
  }
  // Vulnerable: does not check ownership
  c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
}

func fixedHandler(c *gin.Context) {
  user := getCurrentUser(c)
  id, err := strconv.ParseInt(c.Param("id"), 10, 64)
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  res := getResourceByID(id)
  if res == nil {
    c.Status(http.StatusNotFound)
    return
  }
  if res.OwnerID != user.ID {
    c.Status(http.StatusForbidden)
    return
  }
  c.JSON(http.StatusOK, gin.H{"id": res.ID, "data": res.Data})
}

CVE References

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