Overview
Broken Object Property Level Authorization (BOLA) vulnerabilities allow attackers to read, modify, or delete objects owned by other users by guessing or enumerating object identifiers. In real applications this can lead to data leakage, privacy violations, regulatory risk, and reputational damage. When CVEs are not provided in this guide, no CVE IDs are listed here. This pattern remains prevalent in API endpoints that accept an object ID from the client and skip explicit ownership checks.
In Go with the Gin framework, BOLA often shows up when a handler fetches a resource by ID from the URL path and returns it without verifying that the authenticated user has access to that specific object. If ownership or access rights are enforced only implicitly (e.g., via a database row constraint or downstream service) rather than explicitly checked in the API layer, a malicious user can retrieve or modify data belonging to others.
Common signals include endpoints that accept an object ID, perform a lookup, and return the object fields without comparing res.OwnerID to the current user's ID. Stronger patterns, like applying a per-object access check or scoping queries to the current user (WHERE id = ? AND owner_id = ?), reduce this risk but are easy to miss during rapid development.
Remediation approach is to enforce object-level permission checks at the API boundary, implement a reusable authorization helper, and test access control with cross-user scenarios. In Gin, compare the resource.OwnerID with the authenticated user, allow admins if appropriate, and return 403 when access is denied. Centralize logic to avoid duplicating checks across endpoints.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"strconv"
"github.com/gin-gonic/gin"
)
type Resource struct { ID int; OwnerID int; Data string }
var resources = map[int]Resource{
1: {ID: 1, OwnerID: 1, Data: "Alice's public note"},
2: {ID: 2, OwnerID: 2, Data: "Bob's confidential data"},
}
func getCurrentUserID(c *gin.Context) int {
if v := c.GetHeader("X-User-ID"); v != "" {
if id, err := strconv.Atoi(v); err == nil {
return id
}
}
return 0
}
// Vulnerable pattern: returns the object without validating access rights
func vulnerableGetResource(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(400, gin.H{"error": "invalid id"})
return
}
if res, ok := resources[id]; ok {
c.JSON(200, gin.H{"id": res.ID, "data": res.Data, "owner": res.OwnerID})
return
}
c.JSON(404, gin.H{"error": "not found"})
}
// Fixed pattern: enforces per-object authorization
func fixedGetResource(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil { c.JSON(400, gin.H{"error": "invalid id"}); return }
res, ok := resources[id]
if !ok { c.JSON(404, gin.H{"error": "not found"}); return }
userID := getCurrentUserID(c)
role := c.GetHeader("X-User-Role") // optional admin override
if userID != res.OwnerID && role != "admin" {
c.JSON(403, gin.H{"error": "forbidden"})
return
}
c.JSON(200, gin.H{"id": res.ID, "data": res.Data, "owner": res.OwnerID})
}
func main() {
r := gin.Default()
r.GET("/vuln/resources/:id", vulnerableGetResource)
r.GET("/fixed/resources/:id", fixedGetResource)
r.Run(":8080")
}