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
}