Summary
Cloudreve: Path Traversal in WOPI PUT_RELATIVE Allows Arbitrary File Creation in Owner Account
Cloudreve's WOPI PUT_RELATIVE handler treats X-WOPI-SuggestedTarget as a path, not a filename. It splits the header on / and joins the segments onto the source file's directory with URI.JoinRaw, which feeds Go's url.JoinPath. url.JoinPath resolves ./.. segments, so a slash-bearing target such as a/../../evil.docx collapses to a location outside the source file's directory. The lower-level upload path then validates only the final, already-cleaned basename (evil.docx), which is harmless, and checks ownership against the resolved ancestor, which is still the same user's drive.
A WOPI access token is bound to exactly one file (the route enforces fileId == session.FileID with a 403 otherwise). PUT_RELATIVE escapes that per-file scope: a token issued for one file can create (and, conditionally, overwrite) files elsewhere in the same account.
Root cause (verified at 26b6b10)
1. Token is single-file scoped (the boundary being escaped), middleware ViewerSessionValidation:
fileId := hashid.FromContext(c)
if fileId != session.FileID { // 403, token is bound to ONE file
c.Status(http.StatusForbidden); c.Abort(); return
}
Route: wopi := noAuth.Group("file/wopi", middleware.HashID(hashid.FileID), middleware.ViewerSessionValidation()); wopi.POST(":id", controllers.ModifyFile) → POST /api/v4/file/wopi/:id?access_token=<token>.
2. PUT_RELATIVE dispatch, routers/controllers/wopi.go:
case wopi.MethodPutRelative: // X-WOPI-Override: PUT_RELATIVE
err = service.PutContent(c, true)
3. SuggestedTarget joined as a path, service/explorer/viewer.go:
fileName, _ := wopi.UTF7Decode(c.GetHeader(wopi.SuggestedTargetHeader)) // X-WOPI-SuggestedTarget
fileUriParsed, _ := fs.NewUriFromString(fileUri)
if strings.HasPrefix(fileName, ".") { /* treat as extension */ }
fileUri = fileUriParsed.DirUri().JoinRaw(fileName).String() // <-- path join, not basename
...
subService := FileUpdateService{ Uri: fileUri }
res, err := subService.PutContent(c, lockSession)
4. JoinRaw splits on / and normalizes via url.JoinPath, pkg/filemanager/fs/uri.go:
func (u *URI) Join(elem ...string) *URI {
newUrl, _ := url.Parse(u.U.String())
return &URI{U: newUrl.JoinPath(/* PathEscape each elem */ ...)} // JoinPath cleans ./ and ../
}
func (u *URI) JoinRaw(elem string) *URI {
return u.Join(strings.Split(strings.TrimPrefix(elem, Separator), Separator)...)
}
PathEscape leaves . unescaped (it is in the unreserved set), so .. segments survive into JoinPath, which resolves them. URI.Name() returns path.Base(path.Clean(path)), the cleaned basename.
5. Upload checks ownership of the resolved ancestor and validates only the clean basename, pkg/filemanager/fs/dbfs/upload.go:
ancestor, err := f.getFileByPath(ctx, navigator, req.Props.Uri) // URI already traversal-normalized
...
if _, ok := ctx.Value(ByPassOwnerCheckCtxKey{}).(bool); !ok && ancestor.OwnerID() != f.user.ID {
return nil, fs.ErrOwnerOnly // same-user -> passes
}
...
if err := validateNewFile(req.Props.Uri.Name(), req.Props.Size, policy); err != nil { // checks "evil.docx" only
return nil, err
}
validateFileName rejects / \ : * ? " < > | and bare ./.., but the traversal is already gone by the time it sees the basename.
Validation performed
Independent validation against commit 26b6b10 in a clean sandbox.
Source-verified (static): the full chain confirmed verbatim, single-file-scoped token (403 on mismatch) → PUT_RELATIVE dispatch → DirUri().JoinRaw(SuggestedTarget) → url.JoinPath normalization → ancestor ownership check (same-user passes) → basename-only validation of the cleaned name.
Dynamic (control-flow executed): the full binary is not buildable offline here (modules behind an unreachable Go proxy, embedded frontend, DB). I built and ran a harness using the real Go net/url stdlib plus the verbatim Join/JoinRaw/DirUri/Path/Name/PathEscape/shouldEscape and the validateFileName gate, driving the same transformation PUT_RELATIVE performs. Source = cloudreve://my/folder/current.docx:
SuggestedTarget resolved URI final basename validator
"copy.docx" cloudreve://my/folder/copy.docx "copy.docx" ACCEPT
"a/../../evil.docx" cloudreve://my/evil.docx "evil.docx" ACCEPT <- ESCAPED to /
"a/../../../top.docx" cloudreve://my/top.docx "top.docx" ACCEPT <- ESCAPED to /
"sub/evil.docx" cloudreve://my/folder/sub/evil.docx "evil.docx" ACCEPT <- different subdir
".pdf" cloudreve://my/folder/current.pdf "current.pdf" ACCEPT
"a%2f..%2f..%2fenc.docx" cloudreve://my/folder/a%252f..%252f.. "a%2f..%2f..%2f" ACCEPT (NO escape)
The headline payload a/../../evil.docx deterministically resolves to cloudreve://my/evil.docx (account root) with a clean, accepted basename. Output matches the original audit probe exactly. Honest caveat: a leading non-.. segment (e.g. a/) is required to prime the join; a single ../evil.docx does not cleanly escape, and URL-encoded separators (%2f) do not traverse through this path (they are re-escaped into one literal segment). Only literal / separators work.
Confidence tier: source-verified + control-flow dynamically reproduced (no full live HTTP write against a deployed instance).
Deduplication: no existing CVE/GHSA matches. Known Cloudreve advisories are CVE-2022-32167 (XSS, v1–v3.5.3) and CVE-2026-25726 (weak-PRNG ATO, instances initialized < v4.10.0), both unrelated. SECURITY.md lists "user permissions" as high-impact and in scope for all 4.x, so this qualifies as a vulnerability under the project's own policy.
Steps to reproduce
Setup: user owns cloudreve://my/folder/current.docx; open it in the WOPI editor to obtain <token> (the session is bound to that file's ID).
- Send the crafted
PUT_RELATIVE:POST /api/v4/file/wopi/<file-id>?access_token=<token> HTTP/1.1 Host: target X-WOPI-Override: PUT_RELATIVE X-WOPI-SuggestedTarget: <UTF-7 of "a/../../evil.docx"> Content-Type: application/octet-stream <file bytes> - Cloudreve rewrites the target from
cloudreve://my/folder/current.docxtocloudreve://my/evil.docx, validates the basenameevil.docx(passes), and writes the content.
Expected: the target is rejected or constrained to the source file's directory.
Actual: a file is written at the account root, outside the token's single-file scope.
Impact
A WOPI access token scoped to one file can write files to other locations in the same user's account. A malicious or compromised WOPI integration (or a leaked token) can plant or, conditionally, overwrite files at attacker-chosen paths the account owns, defeating the per-file scoping the WOPI session is meant to enforce. Confined to the session user's account (not cross-user).
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.
CVE-2026-55495 has a CVSS score of 4.3 (Medium). 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.0.0-20260613023150-7968e50429ef); 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
- Treat
X-WOPI-SuggestedTarget(andX-WOPI-RequestedName) as a filename, not a path: reject/,\, dot segments, and percent-encoded separator variants before joining. - Prefer
DirUri().Join(sanitizedBaseName)overJoinRaw, and after constructing the target URI assert it is a direct child of the source file's directory. - Add regression tests for
a/../../evil.docx,sub/evil.docx, and encoded-separator variants.
Frequently Asked Questions
- What is CVE-2026-55495? CVE-2026-55495 is a medium-severity path traversal vulnerability in github.com/cloudreve/Cloudreve/v4 (go), affecting versions < 4.0.0-20260613023150-7968e50429ef. It is fixed in 4.0.0-20260613023150-7968e50429ef. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
- How severe is CVE-2026-55495? CVE-2026-55495 has a CVSS score of 4.3 (Medium). 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 packages are affected by CVE-2026-55495?
github.com/cloudreve/Cloudreve/v4(go) (versions < 4.0.0-20260613023150-7968e50429ef)github.com/cloudreve/Cloudreve/v3(go) (versions <= 3.0.0-20250225100611-da4e44b77af4)
- Is there a fix for CVE-2026-55495? Yes. CVE-2026-55495 is fixed in 4.0.0-20260613023150-7968e50429ef. Upgrade to this version or later.
- Is CVE-2026-55495 exploitable, and should I be worried? Whether CVE-2026-55495 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 CVE-2026-55495 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 CVE-2026-55495? Upgrade
github.com/cloudreve/Cloudreve/v4to 4.0.0-20260613023150-7968e50429ef or later.