VYPR
High severityOSV Advisory· Published Feb 2, 2026· Updated Feb 4, 2026

OpenList affected by Path Traversal in file copy and remove handlers

CVE-2026-25059

Description

OpenList Frontend is a UI component for OpenList. Prior to 4.1.10, the application contains path traversal vulnerability in multiple file operation handlers in server/handles/fsmanage.go. Filename components in req.Names are directly concatenated with validated directories using stdpath.Join. This allows ".." sequences to bypass path restrictions, enabling users to access other users' files within the same storage mount and perform unauthorized actions such as deletion, renaming, or copying of files. An authenticated attacker can bypass directory-level authorisation by injecting traversal sequences into filename components, enabling unauthorised file removal and copying across user boundaries within the same storage mount. This vulnerability is fixed in 4.1.10.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/OpenListTeam/OpenList/v4Go
< 4.1.104.1.10

Affected products

1

Patches

1
7b78fed10638

Merge commit from fork

https://github.com/OpenListTeam/OpenListShenLinJan 31, 2026via ghsa
2 files changed · +67 48
  • server/handles/archive.go+3 3 modified
    @@ -231,7 +231,7 @@ func FsArchiveList(c *gin.Context, req *ArchiveListReq, user *model.User) {
     type ArchiveDecompressReq struct {
     	SrcDir        string   `json:"src_dir" form:"src_dir"`
     	DstDir        string   `json:"dst_dir" form:"dst_dir"`
    -	Name          []string `json:"name" form:"name"`
    +	Names         []string `json:"name" form:"name"`
     	ArchivePass   string   `json:"archive_pass" form:"archive_pass"`
     	InnerPath     string   `json:"inner_path" form:"inner_path"`
     	CacheFull     bool     `json:"cache_full" form:"cache_full"`
    @@ -250,8 +250,8 @@ func FsArchiveDecompress(c *gin.Context) {
     		common.ErrorResp(c, errs.PermissionDenied, 403)
     		return
     	}
    -	srcPaths := make([]string, 0, len(req.Name))
    -	for _, name := range req.Name {
    +	srcPaths := make([]string, 0, len(req.Names))
    +	for _, name := range req.Names {
     		srcPath, err := user.JoinPath(stdpath.Join(req.SrcDir, name))
     		if err != nil {
     			common.ErrorResp(c, err, 403)
    
  • server/handles/fsmanage.go+64 45 modified
    @@ -6,18 +6,18 @@ import (
     	"strings"
     
     	"github.com/OpenListTeam/OpenList/v4/internal/conf"
    -	"github.com/OpenListTeam/OpenList/v4/internal/task"
    -
     	"github.com/OpenListTeam/OpenList/v4/internal/errs"
     	"github.com/OpenListTeam/OpenList/v4/internal/fs"
     	"github.com/OpenListTeam/OpenList/v4/internal/model"
     	"github.com/OpenListTeam/OpenList/v4/internal/op"
     	"github.com/OpenListTeam/OpenList/v4/internal/sign"
    +	"github.com/OpenListTeam/OpenList/v4/internal/task"
     	"github.com/OpenListTeam/OpenList/v4/pkg/generic"
     	"github.com/OpenListTeam/OpenList/v4/pkg/utils"
     	"github.com/OpenListTeam/OpenList/v4/server/common"
     	"github.com/gin-gonic/gin"
     	"github.com/pkg/errors"
    +	log "github.com/sirupsen/logrus"
     )
     
     type MkdirOrLinkReq struct {
    @@ -80,36 +80,44 @@ func FsMove(c *gin.Context) {
     		common.ErrorResp(c, errs.PermissionDenied, 403)
     		return
     	}
    -	srcDir, err := user.JoinPath(req.SrcDir)
    -	if err != nil {
    -		common.ErrorResp(c, err, 403)
    -		return
    -	}
     	dstDir, err := user.JoinPath(req.DstDir)
     	if err != nil {
     		common.ErrorResp(c, err, 403)
     		return
     	}
     
    -	var validNames []string
    -	if !req.Overwrite {
    -		for _, name := range req.Names {
    -			if res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil && !req.SkipExisting {
    -				common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", name), 403)
    +	validPaths := make([]string, 0, len(req.Names))
    +	for _, name := range req.Names {
    +		// ensure req.Names is not a relative path
    +		srcPath := stdpath.Join(req.SrcDir, name)
    +		srcPath, err = user.JoinPath(srcPath)
    +		if err != nil {
    +			common.ErrorResp(c, err, 403)
    +			return
    +		}
    +		if !req.Overwrite {
    +			base := stdpath.Base(srcPath)
    +			if base == "." || base == "/" {
    +				common.ErrorStrResp(c, fmt.Sprintf("invalid file name [%s]", name), 400)
     				return
    -			} else if res == nil {
    -				validNames = append(validNames, name)
    +			}
    +			if res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, base), &fs.GetArgs{NoLog: true}); res != nil {
    +				if !req.SkipExisting {
    +					common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", name), 403)
    +					return
    +				} else {
    +					continue
    +				}
     			}
     		}
    -	} else {
    -		validNames = req.Names
    +		validPaths = append(validPaths, srcPath)
     	}
     
     	// Create all tasks immediately without any synchronous validation
     	// All validation will be done asynchronously in the background
     	var addedTasks []task.TaskExtensionInfo
    -	for i, name := range validNames {
    -		t, err := fs.Move(c.Request.Context(), stdpath.Join(srcDir, name), dstDir, len(validNames) > i+1)
    +	for i, p := range validPaths {
    +		t, err := fs.Move(c.Request.Context(), p, dstDir, len(validPaths) > i+1)
     		if t != nil {
     			addedTasks = append(addedTasks, t)
     		}
    @@ -147,44 +155,48 @@ func FsCopy(c *gin.Context) {
     		common.ErrorResp(c, errs.PermissionDenied, 403)
     		return
     	}
    -	srcDir, err := user.JoinPath(req.SrcDir)
    -	if err != nil {
    -		common.ErrorResp(c, err, 403)
    -		return
    -	}
     	dstDir, err := user.JoinPath(req.DstDir)
     	if err != nil {
     		common.ErrorResp(c, err, 403)
     		return
     	}
     
    -	var validNames []string
    -	if !req.Overwrite {
    -		for _, name := range req.Names {
    -			if res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, name), &fs.GetArgs{NoLog: true}); res != nil {
    +	validPaths := make([]string, 0, len(req.Names))
    +	for _, name := range req.Names {
    +		// ensure req.Names is not a relative path
    +		srcPath := stdpath.Join(req.SrcDir, name)
    +		srcPath, err = user.JoinPath(srcPath)
    +		if err != nil {
    +			common.ErrorResp(c, err, 403)
    +			return
    +		}
    +		if !req.Overwrite {
    +			base := stdpath.Base(srcPath)
    +			if base == "." || base == "/" {
    +				common.ErrorStrResp(c, fmt.Sprintf("invalid file name [%s]", name), 400)
    +				return
    +			}
    +			if res, _ := fs.Get(c.Request.Context(), stdpath.Join(dstDir, base), &fs.GetArgs{NoLog: true}); res != nil {
     				if !req.SkipExisting && !req.Merge {
     					common.ErrorStrResp(c, fmt.Sprintf("file [%s] exists", name), 403)
     					return
    -				} else if req.Merge && res.IsDir() {
    -					validNames = append(validNames, name)
    +				} else if !req.Merge || !res.IsDir() {
    +					continue
     				}
    -			} else {
    -				validNames = append(validNames, name)
     			}
     		}
    -	} else {
    -		validNames = req.Names
    +		validPaths = append(validPaths, srcPath)
     	}
     
     	// Create all tasks immediately without any synchronous validation
     	// All validation will be done asynchronously in the background
     	var addedTasks []task.TaskExtensionInfo
    -	for i, name := range validNames {
    +	for i, p := range validPaths {
     		var t task.TaskExtensionInfo
     		if req.Merge {
    -			t, err = fs.Merge(c.Request.Context(), stdpath.Join(srcDir, name), dstDir, len(validNames) > i+1)
    +			t, err = fs.Merge(c.Request.Context(), p, dstDir, len(validPaths) > i+1)
     		} else {
    -			t, err = fs.Copy(c.Request.Context(), stdpath.Join(srcDir, name), dstDir, len(validNames) > i+1)
    +			t, err = fs.Copy(c.Request.Context(), p, dstDir, len(validPaths) > i+1)
     		}
     		if t != nil {
     			addedTasks = append(addedTasks, t)
    @@ -276,18 +288,25 @@ func FsRemove(c *gin.Context) {
     		common.ErrorResp(c, errs.PermissionDenied, 403)
     		return
     	}
    -	reqDir, err := user.JoinPath(req.Dir)
    -	if err != nil {
    -		common.ErrorResp(c, err, 403)
    -		return
    -	}
    -	for _, name := range req.Names {
    -		// Skip invalid item names (empty string, whitespace, ".", "/","\t\t","..") to prevent accidental removal of current directory
    +	for i, name := range req.Names {
     		if strings.TrimSpace(utils.FixAndCleanPath(name)) == "/" {
    -			utils.Log.Warnf("FsRemove: invalid item skipped: %s (parent directory: %s)\n", name, reqDir)
    +			log.Warnf("FsRemove: invalid item skipped: %s (parent directory: %s)\n", name, req.Dir)
    +			req.Names[i] = ""
    +			continue
    +		}
    +		// ensure req.Names is not a relative path
    +		var err error
    +		req.Names[i], err = user.JoinPath(stdpath.Join(req.Dir, name))
    +		if err != nil {
    +			common.ErrorResp(c, err, 403)
    +			return
    +		}
    +	}
    +	for _, path := range req.Names {
    +		if path == "" {
     			continue
     		}
    -		err := fs.Remove(c.Request.Context(), stdpath.Join(reqDir, name))
    +		err := fs.Remove(c.Request.Context(), path)
     		if err != nil {
     			common.ErrorResp(c, err, 500)
     			return
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.