Broken Object Level Authorization

Broken Object Level Authorization in Go (Gin) [Apr 2026] [GHSA-f3h5-h452-vp3j]

[Updated Apr 2026] Updated GHSA-f3h5-h452-vp3j

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

CVE References

Choose which optional cookies to allow. You can change this any time.