Overview
Broken Object Level Authorization (BOLA) in APIs like those built with Go and Gin enables attackers to access resources they do not own by simply altering identifiers in the request path. This can expose sensitive data, enable privilege escalation, and lead to data leaks across tenants when object-level checks are missing. The real-world impact includes unauthorized data exposure, leakage of user-owned records, and potential compliance violations when per-object access controls are not enforced consistently.
In Gin, BOLA commonly arises when handlers fetch resources by ID without verifying ownership or access scope. If queries filter only by id or return full resource data without enforcing authorization, an attacker can enumerate IDs and access others' resources. This risk is amplified in multi-tenant apps, shared resources, or admin endpoints where access boundaries must be strictly enforced.
Remediation and defense-in-depth include: validate ownership on every request, ensure queries enforce owner_id, use authentication context to obtain the caller, centralize authorization checks behind middleware or service layer, and implement tests that simulate cross-user access attempts.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Item struct {
ID int
OwnerID int
Data string
}
var items = []Item{
{ID: 1, OwnerID: 1, Data: "Secret A"},
{ID: 2, OwnerID: 2, Data: "Secret B"},
}
// Vulnerable pattern: fetch by ID without verifying ownership
func vulnGetItem(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.Atoi(idParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
for _, it := range items {
if it.ID == id {
// No ownership check: returns data regardless of who requests it
c.JSON(http.StatusOK, gin.H{"id": it.ID, "owner_id": it.OwnerID, "data": it.Data})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
}
// Fixed pattern: enforce per-object authorization
func fixedGetItem(c *gin.Context) {
// Authenticate caller (example: header-based mock)
userIDHeader := c.GetHeader("X-User-ID")
userID, err := strconv.Atoi(userIDHeader)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthenticated"})
return
}
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
for _, it := range items {
if it.ID == id {
if it.OwnerID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.JSON(http.StatusOK, gin.H{"id": it.ID, "owner_id": it.OwnerID, "data": it.Data})
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
}
func main() {
r := gin.Default()
r.GET("/vuln/items/:id", vulnGetItem)
r.GET("/fix/items/:id", fixedGetItem)
_ = r.Run(":8080")
}