GitHub Advisoryadvisory · Go

GHSA-49h3-cwhj-4737 Cloudreve: Path Traversal in WOPI PUT_RELATIVE Allows Arbitrary File Creation in Owner Account

MEDIUM Published 2026-07-24 Modified 2026-07-24 Official page ↗
Aliases
CVE-2026-55495
Ecosystem
Go
Packages
github.com/cloudreve/Cloudreve/v4, github.com/cloudreve/Cloudreve/v3
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N
Weaknesses
CWE-22
Reviewed
2026-07-24

Affected packages

• Go github.com/cloudreve/Cloudreve/v4: < 4.0.0-20260613023150-7968e50429ef → fixed in 4.0.0-20260613023150-7968e50429ef • Go github.com/cloudreve/Cloudreve/v3: <= 3.0.0-20250225100611-da4e44b77af4 (no fix yet)

Description

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, 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 ` (the session is bound to that file's ID). 1. Send the crafted `PUT_RELATIVE`: ``` POST /api/v4/file/wopi/?access_token= HTTP/1.1 Host: target X-WOPI-Override: PUT_RELATIVE X-WOPI-SuggestedTarget: Content-Type: application/octet-stream ``` 2. Cloudreve rewrites the target from `cloudreve://my/folder/current.docx` to `cloudreve://my/evil.docx`, validates the basename `evil.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). Remediation • Treat `X-WOPI-SuggestedTarget` (and `X-WOPI-RequestedName`) as a filename, not a path: reject `/`, `\`, dot segments, and percent-encoded separator variants before joining. • Prefer `DirUri().Join(sanitizedBaseName)` over `JoinRaw`, 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.

Related

Alias · CVE (1)

  • CVE-2026-55495 Cloudreve is a self-hosted file management and sharing system. vulnerability

Weakness · CWE (1)

  • CWE-22 Improper Limitation of a Pathname to a Restricted Directory weakness

References