Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐹
🐹 Go
Not in CISA KEV
MEDIUM severity

GHSA-49h3-cwhj-4737

MEDIUMFix: cloudreve/cloudreve@7968e50

GHSA-49h3-cwhj-4737 is a medium-severity (CVSS 4.3) Path Traversal vulnerability in github.com/cloudreve/Cloudreve/v4. O3 Security confirms whether GHSA-49h3-cwhj-4737 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-55495GO-2026-6097
Published
Jul 24, 2026
Updated
Aug 18, 2026
Affected
2 pkgs
Patched
1 / 2
Exploits
None indexed
Exploitation data as of Sep 6, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-49h3-cwhj-4737.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs18th percentile — riskier than 18% of all scored CVEsHighest risk
0.00%0.29%0.58%0.88%0.4%0.3%0.3%Aug 26Sep 26Sep 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

GHSA-49h3-cwhj-4737 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 369,023 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

2 pkgs affected
🐹github.com/cloudreve/Cloudreve/v4🐹github.com/cloudreve/Cloudreve/v3

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.

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:

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 dispatchrouters/controllers/wopi.go:

case wopi.MethodPutRelative:            // X-WOPI-Override: PUT_RELATIVE
    err = service.PutContent(c, true)

3. SuggestedTarget joined as a pathservice/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.JoinPathpkg/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 basenamepkg/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).

  1. 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>
    
  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.

Affected Packages

2 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐹Gogithub.com/cloudreve/Cloudreve/v4all versions4.0.0-20260613023150-7968e50429ef
🐹Gogithub.com/cloudreve/Cloudreve/v3all versionsNo fix

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/cloudreve/Cloudreve/v4. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update github.com/cloudreve/Cloudreve/v4 to 4.0.0-20260613023150-7968e50429ef or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-49h3-cwhj-4737 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-49h3-cwhj-4737 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-49h3-cwhj-4737. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

## 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 s
O3 Security · Impact-Aware SCA

Is GHSA-49h3-cwhj-4737 in your dependencies?

O3 detects GHSA-49h3-cwhj-4737 across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-49h3-cwhj-4737: v4 Cross-Site… | O3 Security