Broken Object Level Authorization

Broken Object Level Authorization in Go Gin [Mar 2026] [CVE-2026-28282]

[Updated Mar 2026] Updated CVE-2026-28282

Overview

Broken Object Level Authorization (BOLA) remains a critical risk when APIs fetch resources by ID without confirming the requester’s rights to that specific object. The real-world CVE-2026-28282 in Discourse demonstrates this risk clearly: a user with policy-creation permissions could gain membership access to private groups and subsequently read private, group-restricted topics. This CVE maps to CWE-863 (Broken Object Level Authorization) and shows how insufficient authorization checks enable privilege escalation via object ownership or membership misconfigurations. In practice, this class of vulnerability means that merely authenticating a user is not enough; you must verify per-object access rights.

Affected Versions

Discourse: prior to 2026.3.0-latest.1, 2026.2.1, and 2026.1.2; patched in 2026.3.0-latest.1, 2026.2.1, and 2026.1.2.

Code Fix Example

Go (Gin) API Security Remediation
Vulnerable pattern (Go + Gin):

package main

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

type Document struct {
  ID      int
  GroupID int
  Title   string
  Content string
}

var docs = []Document{
  {ID: 1, GroupID: 10, Title: "Private Doc", Content: "Secret"},
  {ID: 2, GroupID: 20, Title: "Public Doc", Content: "Public"},
}

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

  // Vulnerable: returns document by ID without any per-object authorization check
  r.GET("/documents/:id", func(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil { c.AbortWithStatus(http.StatusBadRequest); return }
    for _, d := range docs {
      if d.ID == id {
        c.JSON(http.StatusOK, d)
        return
      }
    }
    c.AbortWithStatus(http.StatusNotFound)
  })

  // Fixed: enforce object-level access control using per-resource ownership/group checks
  r.GET("/secure/documents/:id", func(c *gin.Context) {
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil { c.AbortWithStatus(http.StatusBadRequest); return }
    var found *Document
    for i := range docs {
      if docs[i].ID == id {
        found = &docs[i]
        break
      }
    }
    if found == nil { c.AbortWithStatus(http.StatusNotFound); return }

    // Demo: retrieve user from header (in production use proper auth middleware)
    userIDStr := c.GetHeader("X-User-ID")
    uid, err := strconv.Atoi(userIDStr)
    if err != nil { c.AbortWithStatus(http.StatusUnauthorized); return }

    // Access check: user must be a member of the document's group
    if !isUserInGroup(uid, found.GroupID) {
      c.AbortWithStatus(http.StatusForbidden)
      return
    }

    c.JSON(http.StatusOK, found)
  })

  r.Run()
}

func isUserInGroup(userID int, groupID int) bool {
  // In-memory example: map user -> groups
  groups, ok := userGroups[userID]
  if !ok { return false }
  for _, g := range groups {
    if g == groupID { return true }
  }
  return false
}

var userGroups = map[int][]int{
  1: {10}, // user 1 belongs to group 10
  2: {20}, // user 2 belongs to group 20
}

CVE References

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