Overview
The CVE-2026-32758 case demonstrates a break in object-level authorization tied to path traversal in a file management context. Authenticated users with Create or Rename permissions could bypass administrator-imposed deny rules by injecting .. sequences into a destination parameter used during a PATCH-like operation. While the destination path is checked against access rules before normalization, the subsequent file operation applies path.Clean(), which resolves the traversal sequences and yields an effective path outside the intended scope. This allows writing or moving files into deny-rule-protected areas within the user’s allowed BasePathFs scope, without escaping that scope or reading restricted paths. The vulnerability is classified under CWE-22 (Path Traversal) and CWE-863 (RCE via BOLOA-related logic), and it was fixed in File Browser version 2.62.0 (CVE-2026-32758).
Affected Versions
File Browser 2.61.2 and below (CVE-2026-32758); fixed in 2.62.0 and later.
Code Fix Example
Go (Gin) API Security Remediation
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"path/filepath"
"os"
"strings"
)
const baseDir = "./files"
type User struct {
ID string
Permissions map[string]bool
}
func getUserFromContext(c *gin.Context) *User {
// In a real app, extract from session/token; this is a minimal stub for demonstration
return &User{ID: "u1", Permissions: map[string]bool{"create": true, "rename": true}}
}
func hasAccess(u *User, action string, path string) bool {
// Placeholder: in real code, implement object-level checks, possibly per-path
return u.Permissions[action]
}
// Vulnerable pattern: authorization check happens on raw input, then path.Clean() is applied only at the file op time
func vulnerablePatchHandler(c *gin.Context) {
user := getUserFromContext(c)
dest := c.PostForm("destPath")
// Authorization based on raw input (vulnerable)
if !hasAccess(user, "rename", dest) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
// Later, normalization happens only for the actual operation
finalPath := filepath.Clean(filepath.Join(baseDir, dest))
os.MkdirAll(filepath.Dir(finalPath), 0755)
if err := os.WriteFile(finalPath, []byte("vulnerable content"), 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"path": finalPath})
}
// Helper to verify path containment after normalization
func isSubPath(base, p string) (bool, error) {
absBase, err := filepath.Abs(base)
if err != nil {
return false, err
}
absP, err := filepath.Abs(p)
if err != nil {
return false, err
}
rel, err := filepath.Rel(absBase, absP)
if err != nil {
return false, err
}
// Ensure the relative path does not go up directories
return !strings.HasPrefix(rel, ".."), nil
}
// Fixed pattern: normalize first, then enforce containment and object-level authorization
func fixedPatchHandler(c *gin.Context) {
user := getUserFromContext(c)
dest := c.PostForm("destPath")
joined := filepath.Join(baseDir, dest)
cleaned := filepath.Clean(joined)
within, err := isSubPath(baseDir, cleaned)
if err != nil || !within {
c.JSON(http.StatusForbidden, gin.H{"error": "destination outside allowed directory"})
return
}
// Authorization on the final, normalized path
if !hasAccess(user, "rename", cleaned) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
os.MkdirAll(filepath.Dir(cleaned), 0755)
if err := os.WriteFile(cleaned, []byte("fixed content"), 0644); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"path": cleaned})
}
func main() {
r := gin.Default()
r.POST("/vuln/patch", vulnerablePatchHandler)
r.POST("/fix/patch", fixedPatchHandler)
r.Run(":8080")
}