Broken Object Property Level Authorization

Broken Object Property Level Authorization and Go (Gin) [CVE-2026-39943]

[Updated Jun 2026] Updated CVE-2026-39943

Overview

Broken Object Property Level Authorization (BOPLA) occurs when an API fails to enforce access controls on individual properties of an object. In Go with Gin, this can manifest as returning an entire resource with sensitive fields (e.g., tokens, secrets, api keys) or allowing property-level access without validating the requester’s rights. The CVE-2026-39943 example from Directus shows how revision history can expose sensitive data when the sanitization or prepareDelta path is not consistently applied, leading to plaintext storage of auth secrets and API keys in revision records. While Directus-specific, the underlying lesson applies broadly: if your API returns or persists data without per-field authorization or proper sanitization, attackers can exfiltrate or leak sensitive properties through normal workflow or revision/history data. In a Go (Gin) context, attackers could manipulate requests to read or update properties they shouldn’t access, or trigger revisions that store sensitive fields unredacted. To fix, implement strict per-property access rules, use dedicated DTOs for outward-facing responses, and sanitize sensitive fields before persisting revisions or audit logs, mirroring the security intent behind CVE-2026-39943 mitigation requirements. This guide demonstrates both vulnerable and fixed patterns and ties them to the CVE reference to emphasize real-world remediation steps.

Affected Versions

Directus 11.x prior to 11.17.0 (CVE-2026-39943); CVE-2026-39943 fixed in 11.17.0

Code Fix Example

Go (Gin) API Security Remediation
package main

import (
  "net/http"
  "strconv"
  "github.com/gin-gonic/gin"
)

type Item struct {
  ID                 int    `json:"id"`
  Name               string `json:"name"`
  PublicInfo         string `json:"public_info"`
  UserToken          string `json:"user_token"`
  TwoFASecret        string `json:"2fa_secret"`
  ExternalAuthID     string `json:"external_auth_id"`
  AuthData           string `json:"auth_data"`
  StoredCredentials  string `json:"stored_credentials"`
  AIProviderAPIKey   string `json:"ai_provider_api_key"`
}

type Revision struct {
  ID       int  `json:"id"`
  Snapshot Item `json:"snapshot"`
}

var items = map[int]Item{
  1: {ID: 1, Name: "Item1", PublicInfo: "Public", UserToken: "secret-token", TwoFASecret: "2fa-secret", ExternalAuthID: "ext-123", AuthData: "auth-data", StoredCredentials: "creds", AIProviderAPIKey: "api-key"},
}

var revisions []Revision

func main() {
  r := gin.Default()

  // Vulnerable: returns full object including sensitive fields
  r.GET("/v1/items/:id", vulnerableGetItem)

  // Fixed: returns a sanitized view exposing only non-sensitive fields
  r.GET("/v2/items/:id", fixedGetItem)

  // Update endpoints: vulnerable vs fixed revision storage
  r.POST("/v1/items/:id/update", vulnerableUpdateItem)
  r.POST("/v2/items/:id/update", fixedUpdateItem)

  r.Run(":8080")
}

func parseID(s string) (int, error) {
  return strconv.Atoi(s)
}

// Vulnerable pattern: returns all fields, including secrets
func vulnerableGetItem(c *gin.Context) {
  id, err := parseID(c.Param("id"))
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  item, ok := items[id]
  if !ok {
    c.Status(http.StatusNotFound)
    return
  }
  c.JSON(http.StatusOK, item)
}

// Fixed view: expose only safe fields
type ItemView struct {
  ID         int    `json:"id"`
  Name       string `json:"name"`
  PublicInfo string `json:"public_info"`
}

func fixedGetItem(c *gin.Context) {
  id, err := parseID(c.Param("id"))
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  item, ok := items[id]
  if !ok {
    c.Status(http.StatusNotFound)
    return
  }
  view := ItemView{ID: item.ID, Name: item.Name, PublicInfo: item.PublicInfo}
  c.JSON(http.StatusOK, view)
}

// Vulnerable update: stores full snapshot including sensitive fields in revisions
func vulnerableUpdateItem(c *gin.Context) {
  id, err := parseID(c.Param("id"))
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  item := items[id]
  revisions = append(revisions, Revision{Snapshot: item})
  c.JSON(http.StatusOK, gin.H{"status": "updated"})
}

// Fixed update: redact sensitive fields before persisting revisions
func fixedUpdateItem(c *gin.Context) {
  id, err := parseID(c.Param("id"))
  if err != nil {
    c.Status(http.StatusBadRequest)
    return
  }
  item := items[id]
  sanitized := item
  sanitized.UserToken = "REDACTED"
  sanitized.TwoFASecret = "REDACTED"
  sanitized.ExternalAuthID = "REDACTED"
  sanitized.AuthData = "REDACTED"
  sanitized.StoredCredentials = "REDACTED"
  sanitized.AIProviderAPIKey = "REDACTED"
  revisions = append(revisions, Revision{Snapshot: sanitized})
  c.JSON(http.StatusOK, gin.H{"status": "updated"})
}

CVE References

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