Broken Object Level Authorization

Broken Object Level Authorization in Go (Gin) [Mar 2026] [GHSA-68j5-4m99-w9w9]

[Updated Mar 2026] Updated GHSA-68j5-4m99-w9w9

Overview

Broken Object Level Authorization (BOLA) in API endpoints can allow an attacker to access or manipulate resources they should not own. In practice, endpoints that rely solely on a provided object identifier (ID) without verifying the requester’s rights enable unauthorized reads, updates, or deletions. This can lead to data leaks, privacy violations, and regulatory risk, especially when objects encode sensitive data. In Go using the Gin framework, handlers commonly fetch a resource by ID from the path and return it directly. If there is no explicit ownership check, the response may reveal data owned by another user. Misconfigurations such as relying on user-supplied IDs, weak guard logic, or missing authorization middleware are common root causes. Remediation relies on enforcing authorization at the resource level. Pull the resource, compare its owner to the authenticated user (or a higher-privilege role) from the request context or token, and return 403 if they do not match. Centralizing the authorization logic in middleware or a dedicated service layer reduces drift and improves testability. Ensure you do not expose ownership details inadvertently by controlling which fields are serialized. Note: While there are no CVEs supplied for this guide, BOLA remains a pervasive risk in Go/Gin apps that expose object endpoints. Apply the patterns below and validate with tests that cover ownership checks for all resource types.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "net/http"
  "strconv"

  "github.com/gin-gonic/gin"
)

type Resource struct {
  ID      int
  OwnerID int
  Data    string
}

var resources = map[int]Resource{
  1: {ID: 1, OwnerID: 42, Data: "secret1"},
  2: {ID: 2, OwnerID: 99, Data: "secret2"},
}

func main() {
  r := gin.Default()

  // mock authentication: set userID in context for demonstration
  r.Use(func(c *gin.Context) {
    c.Set("userID", 42) // in real apps, extract from JWT or session
    c.Next()
  })

  r.GET("/vulnerable/resources/:id", vulnerableHandler)
  r.GET("/fixed/resources/:id", fixedHandler)

  r.Run()
}

func getUserID(c *gin.Context) int {
  if v, ok := c.Get("userID"); ok {
    if id, ok := v.(int); ok {
      return id
    }
  }
  return 0
}

func vulnerableHandler(c *gin.Context) {
  id, err := strconv.Atoi(c.Param("id"))
  if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
    return
  }
  res, ok := resources[id]
  if !ok {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }
  // Vulnerable: no ownership check
  c.JSON(http.StatusOK, res)
}

func fixedHandler(c *gin.Context) {
  id, err := strconv.Atoi(c.Param("id"))
  if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
    return
  }
  res, ok := resources[id]
  if !ok {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }
  userID := getUserID(c)
  if res.OwnerID != userID {
    c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
    return
  }
  c.JSON(http.StatusOK, res)
}

CVE References

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