Overview
Broken Object Property Level Authorization vulnerabilities let attackers read or modify properties on a resource that should be restricted, even when the object itself is accessible. In practice, this means that a GET or PATCH on a user, order, or account may return or expose fields such as balances, personal notes, or internal identifiers that the user should not see. No CVEs are provided for this guide, but the described pattern reflects well-known risks when property-level access is not enforced. When exploited, attackers can gain access to sensitive data, which may lead to financial loss, privacy violations, or regulatory exposure.
In Go applications using Gin, endpoints commonly load an object by ID from the path and respond with the whole struct. If the authorization check only verifies object ownership and does not filter response properties, sensitive fields can be leaked to unauthorized clients. This is especially risky in multi-tenant apps or services handling payments, PII, or internal notes. The breadth of data returned by a single endpoint can inadvertently widen an attacker’s access beyond what is permissible.
This guide demonstrates the vulnerability pattern and a remediation approach. The vulnerable example returns a full User struct; the fixed example uses a DTO with only allowed fields and enforces property-level checks based on the requester’s identity or role. By separating public response models from internal representations, you minimize the risk of inadvertently exporting restricted properties from your API.
For robust defense, implement strict data contracts, include property-level access checks, and test thoroughly. Consider separate handlers or DTOs per view, and verify both object- and property-level authorization in automated tests.
Code Fix Example
Go (Gin) API Security Remediation
Vulnerable pattern:
package main
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
)
type User struct {
ID int
Username string
Email string
Balance float64
SecretNote string
}
type PublicUser struct {
ID int
Username string
Email string
}
func main() {
r := gin.Default()
r.GET("/vuln/users/:id", GetUserVulnerable)
r.GET("/fix/users/:id", GetUserFixed)
r.Run(":8080")
}
func getUserFromDB(id int) *User {
return &User{ID: id, Username: "alice", Email: "[email protected]", Balance: 123.45, SecretNote: "internal"}
}
type Requester struct { ID int; IsAdmin bool }
func getRequester(c *gin.Context) *Requester {
if c.Query("admin") == "1" {
return &Requester{ID: 999, IsAdmin: true}
}
return &Requester{ID: 1, IsAdmin: false}
}
func GetUserVulnerable(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
user := getUserFromDB(id)
c.JSON(http.StatusOK, user) // vulnerable: returns all fields including Balance and SecretNote
}
func GetUserFixed(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.Atoi(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
user := getUserFromDB(id)
req := getRequester(c)
if req.ID != user.ID && !req.IsAdmin {
c.JSON(http.StatusForbidden, map[string]string{"error": "forbidden"})
return
}
safe := PublicUser{ID: user.ID, Username: user.Username, Email: user.Email}
c.JSON(http.StatusOK, safe) // fixed: only public fields are exposed
}