Overview
Note: No CVE IDs are provided for this guide. Broken Object Property Level Authorization (BOPLA) occurs when an API authenticates a user but fails to verify that the user is allowed to access a specific object. In Go applications using the Gin framework, endpoints that read an object ID from the path or request body must enforce per-object permissions; otherwise, an authenticated user could access, modify, or delete resources that belong to others. Without object-level checks, the mere presence of a valid session can become the authorization boundary, leading to data exposure and integrity risks. Real-world impact includes leaking sensitive data, improper edits, or deletions across user boundaries, especially in multi-tenant or partner-integrated systems.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type Resource struct {
ID int64
OwnerID int64
Data string
}
var resources = map[int64]*Resource{
1: {ID: 1, OwnerID: 2, Data: "Alice data"},
2: {ID: 2, OwnerID: 3, Data: "Bob data"},
}
func fetchResource(id int64) *Resource {
if r, ok := resources[id]; ok {
return r
}
return nil
}
func main() {
r := gin.Default()
// Vulnerable pattern: fetch and return without per-object authorization
r.GET("/vuln/resources/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if res := fetchResource(id); res != nil {
c.JSON(http.StatusOK, res)
return
}
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
})
// Fixed pattern: enforce per-object authorization
r.Use(mockAuthMiddleware()) // populate c.Set("userID") and c.Set("isAdmin") for demonstration
r.GET("/resources/:id", func(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
res := fetchResource(id)
if res == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
uidVal, exists := c.Get("userID")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
uid, _ := uidVal.(int64)
if res.OwnerID != uid && !isAdmin(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.JSON(http.StatusOK, res)
})
r.Run(":8080")
}
func mockAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if id := c.GetHeader("X-User-ID"); id != "" {
if parsed, err := strconv.ParseInt(id, 10, 64); err == nil {
c.Set("userID", parsed)
}
}
if c.GetHeader("X-Admin") == "1" {
c.Set("isAdmin", true)
}
}
}
func isAdmin(c *gin.Context) bool {
if v, ok := c.Get("isAdmin"); ok {
if b, ok := v.(bool); ok {
return b
}
}
return false
}