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()
}