Broken Object Property Level Authorization

Broken Object Property Level Authorization in Go (Gin) [GHSA-r297-p3v4-wp8m]

[Updated March 2026] Updated GHSA-r297-p3v4-wp8m

Overview

Broken Object Property Level Authorization in REST APIs, including Go with Gin, occurs when an application relies on client-provided identifiers without verifying that the requester owns or is permitted to access the targeted resource. In production, this can let an attacker read, modify, or delete resources that belong to other users, escalate privileges, or exfiltrate sensitive data by simply changing an ID in the URL, body, or query. The impact is particularly severe in multi-tenant or role-based systems where access control is not tightly scoped to the authenticated user. In Go (Gin) apps, handlers frequently take an object ID from the route (e.g., /resource/:id) and perform a data lookup before applying authorization. If the query only filters by id and not by owner_id/tenant_id, the attacker can access unauthorized resources. Even when a check exists, if it happens after data retrieval or if only a property is validated, attackers may gain unintended access by manipulating related fields. Attackers can also enumerate IDs in predictable sequences to harvest data. Remediation strategy: enforce authorization in the data access path rather than in a post-check, and tie every read/write/delete operation to the authenticated subject. Use the authenticated user (or their roles) to constrain queries (e.g., SELECT ... WHERE id = ? AND owner_id = ?). Validate IDs, avoid exposing sensitive properties, and centralize OLAC logic in middleware or a reusable service. Add tests that simulate cross-user access attempts. Adopt robust patterns: implement middleware to populate a user context; implement OLAC in repository/service layer; consider policy-based access control; enable row-level security in the database; log violations; perform regular security testing and code reviews focusing on ID-based access.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "net/http"
  "sync"

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

type Resource struct {
  ID      string
  OwnerID string
  Data    string
}

type InMemoryStore struct {
  mu   sync.RWMutex
  data map[string]Resource
}

var store = InMemoryStore{
  data: map[string]Resource{
    "1": {ID: "1", OwnerID: "alice", Data: "Secret A"},
    "2": {ID: "2", OwnerID: "bob", Data: "Secret B"},
  },
}

func AuthMiddleware() gin.HandlerFunc {
  return func(c *gin.Context) {
    user := c.GetHeader("X-User-ID")
    if user == "" {
      c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
      return
    }
    c.Set("userID", user)
    c.Next()
  }
}

// Vulnerable pattern: reads by id and returns resource without checking ownership
func getResourceVulnerable(c *gin.Context) {
  id := c.Param("id")
  store.mu.RLock()
  r, ok := store.data[id]
  store.mu.RUnlock()
  if !ok {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }
  // Vulnerable: no ownership check
  c.JSON(http.StatusOK, r)
}

// Fixed pattern: enforces ownership by constraining access to the authenticated user
func getResourceFixed(c *gin.Context) {
  id := c.Param("id")
  val, exists := c.Get("userID")
  if !exists {
    c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
    return
  }
  owner := val.(string)
  store.mu.RLock()
  r, ok := store.data[id]
  store.mu.RUnlock()
  if !ok || r.OwnerID != owner {
    c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
    return
  }
  c.JSON(http.StatusOK, r)
}

func main() {
  r := gin.Default()
  r.Use(AuthMiddleware())
  r.GET("/vuln/resource/:id", getResourceVulnerable)
  r.GET("/fix/resource/:id", getResourceFixed)
  r.Run(":8080")
}

CVE References

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