Overview
Broken object level authorization (BOLOA) is a common API vulnerability where an attacker can access resources they do not own by manipulating object IDs. In Go services built with the Gin framework, endpoints frequently accept IDs via path parameters and return data or perform actions without strict ownership checks. The real-world impact includes data leakage, privacy violations, and potential regulatory risk when sensitive resources are exposed to unauthorized users.
BOLOA manifests in Gin when authorization checks are performed after loading the object, rely on unsafe constraints, or are implemented per endpoint rather than as a centralized policy. Patterns include fetching by id and then comparing owner_id to the current user, or issuing queries that do not filter by owner or tenant, enabling attackers to enumerate and access other users' resources.
Mitigation requires consistent, early ownership validation, centralizing authorization logic, and leveraging database constraints or row-level security where available. Enforce per-user or per-tenant access at the boundary, and write tests that cover forbidden access and positive access across all object types.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Resource struct {
ID int64
OwnerID int64
Data string
}
var resources = []Resource{
{ID: 1, OwnerID: 100, Data: "secret1"},
{ID: 2, OwnerID: 200, Data: "secret2"},
}
func getResourceByID(id int64) *Resource {
for i := range resources {
if resources[i].ID == id {
return &resources[i]
}
}
return nil
}
func getUserIDFromHeader(c *gin.Context) int64 {
if v := c.GetHeader(`X-User-ID`); v != "" {
if n, err := strconv.ParseInt(v, 10, 64); err == nil {
return n
}
}
return 0
}
func main() {
r := gin.Default()
// Vulnerable: no ownership check
r.GET(`/vulnerable/resources/:id`, func(c *gin.Context){
id, _ := strconv.ParseInt(c.Param(`id`), 10, 64)
res := getResourceByID(id)
if res == nil {
c.JSON(http.StatusNotFound, gin.H{`error`: `not found`})
return
}
c.JSON(http.StatusOK, gin.H{`id`: res.ID, `data`: res.Data})
})
// Fixed: verify ownership before serving
r.GET(`/fixed/resources/:id`, func(c *gin.Context){
userID := getUserIDFromHeader(c)
id, _ := strconv.ParseInt(c.Param(`id`), 10, 64)
res := getResourceByID(id)
if res == nil {
c.JSON(http.StatusNotFound, gin.H{`error`: `not found`})
return
}
if res.OwnerID != userID {
c.JSON(http.StatusForbidden, gin.H{`error`: `forbidden`})
return
}
c.JSON(http.StatusOK, gin.H{`id`: res.ID, `data`: res.Data})
})
r.Run(`:8080`)
}