CVE-2026-55495
Cloudreve: Path Traversal in WOPI PUT_RELATIVE Allows Arbitrary File Creation in Owner Account
Summary
## Summary 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`: ```go 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= `. **2. `PUT_RELATIVE` dispatch** — `routers/controllers/wopi.go`: ```go case wopi.MethodPutRelative: // X-WOPI-Override: PUT_RELATIVE err = service.PutContent(c, true) ``` **3. SuggestedTarget joined as a path** — `service/explorer/viewer.go`: ```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() // 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, embedd
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| Go | github.com/cloudreve/Cloudreve/v4 | — | 4.0.0-20260613023150-7968e50429ef |
| Go | github.com/cloudreve/Cloudreve/v3 | — | — |
Remediation: Upgrade to 4.0.0-20260613023150-7968e50429ef or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.
CVE® is a registered trademark of The MITRE Corporation. CVE content reproduced under the CVE Terms of Use; copyright designation © MITRE.