Overview
Broken Object Level Authorization (BOLA) occurs when a system fails to verify that the authenticated user is authorized to access a specific object. In practice, this lets attackers read, modify, or delete other users' data by guessing or manipulating object IDs in requests. In Go applications using Gin, a tempting pattern is to trust the object ID in the URL and rely on authentication alone, assuming the user can't access other IDs.\n\nReal-world impact includes leakage of sensitive records, financial data, or personal information. An attacker may enumerate resource IDs and extract data or perform unauthorized actions.\n\nManifestation in Gin: If handlers perform a query by id only and then return the result, or construct queries like find where id = ? without checking ownership, BOLA occurs. Typical endpoints include /resources/:id, /orders/:id. Attackers can craft requests with valid IDs to access others' resources.\n\nRemediation approach: Enforce per-object authorization checks in every handler by tying the object to the current user. Always include owner or access scope in data queries (e.g., where id = ? and owner_id = ?). Consider middleware that loads the current user and use repository/service layer checks. Add unit/integration tests that cover positive and negative cases for many objects.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey"`
Name string
}
type Resource struct {
ID uint `gorm:"primaryKey"`
OwnerID uint
Data string
}
var db *gorm.DB
func main() {
var err error
db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil { log.Fatal(err) }
db.AutoMigrate(&User{}, &Resource{})
// Seed
db.Create(&User{ID:1, Name:"Alice"})
db.Create(&User{ID:2, Name:"Bob"})
db.Create(&Resource{ID:1, OwnerID:1, Data:"TopSecret1"})
db.Create(&Resource{ID:2, OwnerID:2, Data:"TopSecret2"})
r := gin.Default()
// simple auth middleware for demo purposes
r.Use(func(c *gin.Context) {
id := c.GetHeader("X-User-ID")
if id == "" {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
var u User
if id == "1" {
u = User{ID:1, Name:"Alice"}
} else if id == "2" {
u = User{ID:2, Name:"Bob"}
} else {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
c.Set("user", u)
c.Next()
})
r.GET("/v1/resources/:id", vulnerableHandler)
r.GET("/v1/resources-secure/:id", secureHandler)
log.Fatal(r.Run(":8080"))
}
func vulnerableHandler(c *gin.Context) {
id := c.Param("id")
var res Resource
if err := db.First(&res, "id = ?", id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
// Vulnerable: no ownership check
c.JSON(http.StatusOK, res)
}
func secureHandler(c *gin.Context) {
id := c.Param("id")
userVal, exists := c.Get("user")
if !exists {
c.AbortWithStatus(http.StatusUnauthorized)
return
}
user := userVal.(User)
var res Resource
// Enforce ownership in query
if err := db.First(&res, "id = ? AND owner_id = ?", id, user.ID).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, res)
}