A AegiFlow
MEDIUMCVSS 4.3

CVE-2026-55495

Cloudreve: Path Traversal in WOPI PUT_RELATIVE Allows Arbitrary File Creation in Owner Account

Published
2026-07-24
Modified
2026-07-24
Aliases
GHSA-49h3-cwhj-4737
Sources
github-advisory

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

EcosystemPackageAffected versionsFixed versions
Gogithub.com/cloudreve/Cloudreve/v44.0.0-20260613023150-7968e50429ef
Gogithub.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.