Overview
CVE-2026-39942 describes a real-world scenario in Directus where the PATCH /files/{id} endpoint accepts a user-controlled filename_disk parameter. An attacker could set this value to match the storage path of another user's file, allowing them to overwrite that file's content while also manipulating metadata fields such as uploaded_by to obscure tampering. This is a classic Broken Object Level Authorization (BOLOA) pattern: authorization is not checked against the specific object the operation targets, enabling cross-user modification when the API lacks per-object access validation (CWE-284, CWE-639). In practice, such an issue can lead to data loss, data integrity problems, and misleading ownership metadata. The CVE fixes move the logic into server-controlled storage paths and enforce ownership checks to prevent unauthorized object mutations. In Go with Gin, a vulnerable pattern might still allow a client to supply a disk path and metadata changes tied to a specific file ID without verifying that the requester owns that file. The remediation is to bind path and state changes to a server-side mapping and verify object ownership before applying updates.
Affected Versions
Directus prior to 11.17.0 (CVE-2026-39942)
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type File struct {
ID string
OwnerID string
Path string
Metadata map[string]string
}
func main() {
r := gin.Default()
// In-memory store of files (simulated data layer)
files := map[string]File{
"f1": {ID: "f1", OwnerID: "u1", Path: "/tmp/storage/u1_f1.dat", Metadata: map[string]string{"uploaded_by": "u1"}},
}
// Simple auth: injects userID into context
r.Use(func(c *gin.Context) {
c.Set("userID", "u1")
c.Next()
})
// Vulnerable endpoint: user-supplied filename_disk is written directly and metadata may be updated without ownership checks
r.PATCH("/files/:id", func(c *gin.Context) {
id := c.Param("id")
var req struct {
FilenameDisk string `json:"filename_disk"`
Content string `json:"content"`
Metadata map[string]string `json:"metadata"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
if req.FilenameDisk != "" {
// Vulnerable: the client can direct where to write data
_ = os.WriteFile(req.FilenameDisk, []byte(req.Content), 0644)
if f, ok := files[id]; ok {
f.Path = req.FilenameDisk
if v, ok := req.Metadata["uploaded_by"]; ok {
f.Metadata["uploaded_by"] = v
}
files[id] = f
}
}
c.JSON(http.StatusOK, gin.H{"status": "updated (vulnerable)"})
})
// Fixed: enforce per-object authorization and server-side storage path derivation
r.PATCH("/files_secure/:id", func(c *gin.Context) {
id := c.Param("id")
val, _ := c.Get("userID")
userID := val.(string)
f, ok := files[id]
if !ok {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if f.OwnerID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
var req struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
// Server-controlled path derived from the resource ID
path := "/tmp/storage/" + id
if err := os.WriteFile(path, []byte(req.Content), 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "write failed"})
return
}
f.Path = path
f.Metadata["uploaded_by"] = userID
files[id] = f
c.JSON(http.StatusOK, gin.H{"status": "updated securely"})
})
r.Run(":8080")
}