Overview
CVE-2026-42883 describes a broken object level authorization in Audiobookshelf where the GET /api/libraries/:id/download endpoint validates the user’s access to the library in the URL but fetches downloadable item data by attacker-provided IDs without scoping to that library. An authenticated user with download permission and access to any one library can exfiltrate the full contents of items from other libraries to which they should be denied. This class of vulnerability illustrates how loose server-side authorization checks enable cross-library data leakage (CWE-863). The issue was fixed in version 2.32.2; applying the same security principle in Go with Gin requires enforcing object-level authorization on each item in the response.
Fixed in Audiobookshelf 2.32.2, but the same pattern can appear in Go applications using Gin if the code fetches items by IDs without validating their library association. In a Go/Gin API, the same flaw occurs when the handler verifies library access only at the library level but returns items identified by IDs that may belong to other libraries the user should not access.
To remediate in Go, enforce object-level authorization by validating that every item being downloaded belongs to the requested library and that the user has permission for that item's library before including it in the response. Prefer a query that scopes items by both id and library, or perform per-item checks after loading the item, and fail the request if any item is out of scope.
Testing and code review should ensure that any endpoint that accepts an external list of object IDs applies access checks to every object. Add unit tests that simulate cross-library item IDs attempting to leak data and verify the API returns 403/404 rather than exposing content.
Affected Versions
2.32.1 and earlier
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
type User struct { ID string }
type Item struct { ID string; LibraryID string; Name string }
var sampleItems = map[string]Item{
"a": {ID: "a", LibraryID: "lib1", Name: "Alpha"},
"b": {ID: "b", LibraryID: "lib2", Name: "Beta"},
}
func main() {
r := gin.Default()
r.GET("/api/libraries/:id/download", vulnerableDownload)
r.GET("/api/libraries/:id/download_fixed", fixedDownload)
_ = r.Run(":8080")
}
func vulnerableDownload(c *gin.Context) {
libID := c.Param("id")
user := &User{ID: "u1"}
if !userHasAccessToLibrary(user, libID) {
c.AbortWithStatus(http.StatusForbidden)
return
}
ids := c.QueryArray("ids")
var items []Item
for _, id := range ids {
if it, ok := sampleItems[id]; ok {
items = append(items, it)
}
}
// vulnerable: returns items regardless of their library
c.JSON(http.StatusOK, gin.H{"count": len(items)})
}
func fixedDownload(c *gin.Context) {
libID := c.Param("id")
user := &User{ID: "u1"}
if !userHasAccessToLibrary(user, libID) {
c.AbortWithStatus(http.StatusForbidden)
return
}
ids := c.QueryArray("ids")
var items []Item
for _, id := range ids {
if it, ok := sampleItems[id]; ok && it.LibraryID == libID {
items = append(items, it)
}
}
if len(items) == 0 {
c.AbortWithStatus(http.StatusNotFound)
return
}
c.JSON(http.StatusOK, gin.H{"count": len(items)})
}
func userHasAccessToLibrary(u *User, libID string) bool { return true }