Broken Object Property Level Authorization

Broken Object Property Level Authorization in Go (Gin)

[Updated month year] Updated

Overview

Broken Object Property Level Authorization in Go (Gin) can allow unauthorized access to resources when endpoints rely on client-provided object IDs without verifying ownership. Real-world impact includes data leakage, privilege escalation, and regulatory risk in multi-tenant or sensitive domains. In Gin-based Go applications, this vulnerability often happens when a handler fetches an object by ID from a path parameter and returns it without verifying that the current user owns the object. This can occur when queries lack an owner filter or when authorization checks are skipped. Remediation focuses on enforcing object-level access control in handlers and data stores. Establish a reliable user identity from your JWT or session, and ensure every read/update/delete includes a strict ownership check or a policy-based RBAC check with query filtering. The code sample in this guide shows a vulnerable pattern and a fixed pattern: the vulnerable handler returns the object after a basic fetch without ownership verification; the fixed handler enforces ownership before returning or denies access with 403/404, illustrating the recommended approach.

Code Fix Example

Go (Gin) API Security Remediation
Vulnerable vs Fixed Go (Gin) example:

VULNERABLE:
package main

import (
 `net/http`
 `strconv`

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

type User struct { ID int }
type Order struct { ID int; OwnerID int; Item string }

var orders = []Order{{ID:1, OwnerID:1, Item:`Book`}, {ID:2, OwnerID:2, Item:`Phone`}}

func main() {
 r := gin.Default()
 // mock authentication: attach user to context
 r.Use(func(c *gin.Context){ c.Set(`user`, User{ID:1}); c.Next() })
 r.GET(`/orders/:id`, vulnerableGetOrder)
 r.GET(`/orders_fixed/:id`, fixedGetOrder)
 r.Run(`:8080`)
}

func vulnerableGetOrder(c *gin.Context) {
 id, err := strconv.Atoi(c.Param(`id`))
 if err != nil { c.JSON(http.StatusBadRequest, gin.H{`error`: `invalid id`}); return }
 var found *Order
 for i := range orders {
  if orders[i].ID == id { found = &orders[i]; break }
 }
 if found == nil { c.JSON(http.StatusNotFound, gin.H{`error`: `order not found`}); return }
 // Vulnerability: no ownership check
 c.JSON(http.StatusOK, found)
}

func fixedGetOrder(c *gin.Context) {
 id, err := strconv.Atoi(c.Param(`id`))
 if err != nil { c.JSON(http.StatusBadRequest, gin.H{`error`: `invalid id`}); return }
 var found *Order
 for i := range orders {
  if orders[i].ID == id { found = &orders[i]; break }
 }
 if found == nil { c.JSON(http.StatusNotFound, gin.H{`error`: `order not found`}); return }
 user, _ := c.Get(`user`)
 current := user.(User)
 if found.OwnerID != current.ID {
  c.JSON(http.StatusForbidden, gin.H{`error`: `forbidden`})
  return
 }
 c.JSON(http.StatusOK, found)
}

FIXED:
package main

import (
 `net/http`
 `strconv`

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

type User struct { ID int }
type Order struct { ID int; OwnerID int; Item string }

var orders = []Order{{ID:1, OwnerID:1, Item:`Book`}, {ID:2, OwnerID:2, Item:`Phone`}}

func main() {
 r := gin.Default()
 r.Use(func(c *gin.Context){ c.Set(`user`, User{ID:1}); c.Next() })
 r.GET(`/orders/:id`, vulnerableGetOrder)
 r.GET(`/orders_fixed/:id`, fixedGetOrder)
 r.Run(`:8080`)
}

func vulnerableGetOrder(c *gin.Context) {
 id, err := strconv.Atoi(c.Param(`id`))
 if err != nil { c.JSON(http.StatusBadRequest, gin.H{`error`: `invalid id`}); return }
 var found *Order
 for i := range orders {
  if orders[i].ID == id { found = &orders[i]; break }
 }
 if found == nil { c.JSON(http.StatusNotFound, gin.H{`error`: `order not found`}); return }
 // Vulnerable path retained for contrast; in real code, ownership would be checked
 c.JSON(http.StatusOK, found)
}

func fixedGetOrder(c *gin.Context) {
 id, err := strconv.Atoi(c.Param(`id`))
 if err != nil { c.JSON(http.StatusBadRequest, gin.H{`error`: `invalid id`}); return }
 var found *Order
 for i := range orders {
  if orders[i].ID == id { found = &orders[i]; break }
 }
 if found == nil { c.JSON(http.StatusNotFound, gin.H{`error`: `order not found`}); return }
 user, _ := c.Get(`user`)
 current := user.(User)
 if found.OwnerID != current.ID {
  c.JSON(http.StatusForbidden, gin.H{`error`: `forbidden`})
  return
 }
 c.JSON(http.StatusOK, found)
}

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