Broken Object Property Level Authorization

Broken Object Property Level Authorization in Go (Gin) [GHSA-hff2-gcpx-8f4p]

[Updated Jun 2026] Updated GHSA-hff2-gcpx-8f4p

Overview

Broken Object Property Level Authorization (BOPLA) in web APIs can allow an attacker to access, modify, or delete resources they do not own by altering object identifiers in requests. When the server trusts client-supplied IDs without validating ownership, sensitive data can be exposed, operations performed on another user’s data, and regulatory/compliance obligations may be violated. In production, such flaws often slip in as subtle logic errors or missing checks in resource endpoints, escalating risk as the system scales. In Go with the Gin framework, BOPLA typically manifests in handlers that read an object id from the URL (for example /resources/:id) and perform a fetch or mutation using that id before verifying the requester’s rights. If authorization is checked only after retrieving the object, or if checks rely on data the client could manipulate, an attacker can enumerate or alter other users' resources by substituting a different id in the request. The problem is easier to exploit when endpoints share access patterns and do not enforce ownership consistently across handlers. Remediation focuses on enforcing ownership checks in every object-level operation. This means tying the data retrieval and mutation to the authenticated user, enforcing ownership at the repository or DB layer, using middleware or a policy engine, and thoroughly testing with negative cases. Logging and monitoring authorization failures help detect exploitation attempts and improve safety.

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
    "log"
    "net/http"

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

type Resource struct {
    ID      string
    OwnerID string
    Data    string
}

// Vulnerable pattern: does not verify ownership
func vulnerableGetResource(c *gin.Context) {
    id := c.Param("id")
    // Pretend we obtained userID from a JWT middleware
    userID := c.GetString("userID")
    if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing id"}); return }
    res, err := fetchResourceByID(id) // no ownership check here
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
        return
    }
    _ = userID
    c.JSON(http.StatusOK, res)
}

// Fixed pattern: verify ownership before returning
func fixedGetResource(c *gin.Context) {
    id := c.Param("id")
    userID := c.GetString("userID")
    if id == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing id"}); return }
    res, err := fetchResourceByID(id)
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
        return
    }
    if res.OwnerID != userID {
        c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
        return
    }
    c.JSON(http.StatusOK, res)
}

func main() {
    r := gin.Default()
    // Example routes; in production, the userID would be set by auth middleware
    r.GET("/resources/:id", vulnerableGetResource) // vulnerable endpoint
    r.GET("/resources/:id/secured", fixedGetResource) // secured endpoint
    if err := r.Run(":8080"); err != nil {
        log.Fatal(err)
    }
}

// Placeholder DB fetch; in real apps use parameterized queries and ownership checks in DB
func fetchResourceByID(id string) (Resource, error) {
    // Mock: resource owned by user-1
    return Resource{ID: id, OwnerID: "user-1", Data: "example"}, nil
}

CVE References

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