Broken Object Property Level Authorization

Broken Object Property Level Authorization in Go (Gin) [GHSA-rj4g-rqgh-rx9h]

[Updated May 2026] Updated GHSA-rj4g-rqgh-rx9h

Overview

Broken Object Property Level Authorization (BOPLA) occurs when an API validates that a caller can access a resource as a whole but fails to enforce access on individual properties within that object. In Go applications using the Gin framework, handlers often fetch a resource by ID and return a struct as JSON without filtering fields the caller isn't allowed to see. Attackers can manipulate IDs or request different resources to observe what properties are exposed, which can leak sensitive data unexpectedly. This class of vulnerability is particularly dangerous in multi-tenant or role-based scenarios where some properties should remain private even if the top-level object is accessible. The real-world impact includes leakage of sensitive fields (like internal notes, secrets, or cross-owner metadata) and potential privilege escalation when multiple tenants share a single object but with properties restricted by the owner. Because Go structs are serialized to JSON directly, failing to redact per-property data means a single endpoint can unintentionally reveal data from other users or roles, widening the blast radius of a breach. This vulnerability often arises when developers rely on object-level authorization but forget to apply property-level checks to nested fields or sub-objects. In gin-based services, BOPLA typically stems from returning DB models directly or combining object-level checks with loose visibility rules. Developers may forget to filter nested fields or to construct per-property access rules, especially when using embedded structs, maps, or dynamically assembled responses. The secure approach is to model explicit response types (DTOs) and enforce field-level access controls, redacting or omitting properties based on the caller’s identity and permissions.

Code Fix Example

Go (Gin) API Security Remediation
VULNERABLE:
package main

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

type Resource struct {
  ID      int64
  OwnerID int64
  Public  string
  Private string
  Secret  string
}

func getResourceFromDB(id int64) (*Resource, error) {
  // This is a stub; in a real app, fetch from DB
  return &Resource{ID: id, OwnerID: 42, Public: "pub", Private: "priv", Secret: "secret"}, nil
}

func VulnerableGetResource(c *gin.Context) {
  idStr := c.Param("id")
  id, err := strconv.ParseInt(idStr, 10, 64)
  if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
    return
  }
  res, err := getResourceFromDB(id)
  if err != nil {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }

  // Vulnerable: returns full resource including sensitive fields
  c.JSON(http.StatusOK, res)
}

func main() {
  r := gin.Default()
  // For demonstration, a mock auth middleware could be added here
  r.GET("/resources/:id", VulnerableGetResource)
  r.Run()
}

FIX:
package main

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

type Resource struct {
  ID      int64
  OwnerID int64
  Public  string
  Private string
  Secret  string
}

func getResourceFromDB(id int64) (*Resource, error) {
  return &Resource{ID: id, OwnerID: 42, Public: "pub", Private: "priv", Secret: "secret"}, nil
}

func getUserIDFromContext(c *gin.Context) (int64, bool) {
  v, ok := c.Get("userID")
  if !ok {
    return 0, false
  }
  uid, ok := v.(int64)
  return uid, ok
}

func FixedGetResource(c *gin.Context) {
  idStr := c.Param("id")
  id, err := strconv.ParseInt(idStr, 10, 64)
  if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
    return
  }
  res, err := getResourceFromDB(id)
  if err != nil {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }

  userID, ok := getUserIDFromContext(c)
  if !ok {
    c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
    return
  }

  if res.OwnerID != userID {
    // redact sensitive properties for non-owners
    redacted := Resource{
      ID:      res.ID,
      OwnerID: res.OwnerID,
      Public:  res.Public,
      Private: "",
      Secret:  "",
    }
    c.JSON(http.StatusOK, redacted)
    return
  }

  c.JSON(http.StatusOK, res)
}

func mockAuthMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    // For demonstration purposes only: pretend user 7 is authenticated
    c.Set("userID", int64(7))
    c.Next()
  }
}

func main() {
  r := gin.Default()
  r.Use(mockAuthMiddleware())
  r.GET("/resources/:id", FixedGetResource)
  r.Run()
}

CVE References

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