Résumé
Cloudreve has Broken Access Control - Revoked Share Access Still Allows Signed File URL Generation via Cached context_hint
Détails de l’avis
Summary
Cloudreve's file-listing responses hand the client a context_hint (UUID) that is meant to speed up follow-up operations. When that hint is replayed on the file/url (and file/thumb) routes, DBFS caches a shareNavigatorState containing the already-loaded share root and share row.
On a later request carrying the same hint, shareNavigator.RestoreState repopulates shareRoot, and shareNavigator.To then skips Root. Root is the only place that re-checks inventory.IsValidShare (share expiry, remaining-download count, owner status, source-file validity) and the share password. As a result, a recipient who prewarms a context hint while access is valid can keep minting signed file URLs for already-known shared file paths for up to the context-hint TTL (5 * 60 = 300 s) after the owner deletes the share or the share expires — plus the lifetime of any signed entity URL minted in that window.
This is a revocation / expiry bypass, not a way to discover unknown share contents: the attacker must already have had access to the share and must know the target file URI from a prior listing.
Root cause (verified at 26b6b10)
1. List responses leak the hint and each file URI — service/explorer/response.go populates ListResponse.ContextHint and FileResponse.Path (f.Uri(false).String()).
2. file/url and file/thumb accept the client-supplied hint — routers/router.go:631 and :662:
file.POST("url", middleware.ContextHint(), /* ... */ controllers.FileURL)
file.GET("thumb", middleware.ContextHint(), /* ... */ controllers.Thumb)
The file group's only auth gate is middleware.RequiredScopes(types.ScopeFilesRead) — there is no independent share-validation middleware on this route. All share validation lives inside DBFS.
3. The middleware trusts the header verbatim — middleware/file.go:41:
func ContextHint() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader(dbfs.ContextHintHeader) != "" { // X-Cr-Context-Hint
util.WithValue(c, dbfs.ContextHintCtxKey{}, uuid.FromStringOrNil(c.GetHeader(dbfs.ContextHintHeader)))
}
c.Next()
}
}
4. DBFS restores cached navigator state on a hint hit — dbfs.go:745 (ContextHintTTL = 5 * 60, dbfs.go:34). On a miss it arms PersistState; the closure fires in DBFS.Recycle() at end of request.
5. Persisted share state carries the loaded shareRoot + share row — share_navigator.go:72/:85. RestoreState reinstates n.shareRoot, n.share, n.owner, etc.
6. Root is the sole validity/password gate — share_navigator.go:114 → inventory.IsValidShare(share) (inventory/share.go:227: IsShareExpired checks Expires.Before(now) and RemainDownloads <= 0, plus owner-active and source-file checks) followed by the share.Password comparison.
7. To skips Root once shareRoot is set — share_navigator.go:181:
func (n *shareNavigator) To(ctx context.Context, path *fs.URI) (*File, error) {
if n.shareRoot == nil { // restored state => NOT nil => Root() skipped
root, err := n.Root(ctx, path)
...
}
...
}
The single-file-share branch is also affected: it calls latestSharedSingleFile, which fetches n.fileClient.GetByID(n.share.Edges.File.ID) straight from the restored share with no revalidation (share_navigator.go).
8. A failed download hook does not block URL issuance — pkg/filemanager/manager/entity.go:250:
if err := m.fs.ExecuteNavigatorHooks(ctx, fs.HookTypeBeforeDownload, file); err != nil {
m.l.Warning("Failed to execute navigator hooks: %s", err) // logged, NOT fatal
}
The share's BeforeDownload hook is shareClient.Downloaded() (UpdateOneID(share.ID).AddDownloads(1).AddRemainDownloads(-1)). Against a deleted share this update errors, but the error is only logged and the signed URL is still minted. The signed content endpoint file/content/:id/... is then guarded only by middleware.SignRequired — it does not re-check the share.
Steps to reproduce
Setup: one share owner; one recipient (a second free account, or anonymous if the default anon group keeps share-download). Recipient knows the share URL (and password, if any).
- Recipient lists the valid share:
Response includesGET /api/v4/file?uri=<share-uri> HTTP/1.1 Host: targetcontext_hintand each file'spath. - While the share is still valid, recipient warms the cache for a known file:
(cache MISS →POST /api/v4/file/url HTTP/1.1 Host: target X-Cr-Context-Hint: <context_hint> Content-Type: application/json {"uri":["<known-shared-file-uri>"]}Rootruns →PersistStatearmed →RecyclewritesshareNavigatorStateto KV undernavigator_state_<hint>_share.) - Owner deletes the share, or it expires / hits zero remaining downloads.
- Within 300 s, recipient repeats the same request from step 2 (same
X-Cr-Context-Hint, same URI). (cache HIT →RestoreStatesetsshareRoot→ToskipsRoot→IsValidSharenever runs → signed entity URL returned.) - The signed URL serves the file content;
file/content/:id/...validates only the signature. Expected: step 4 returnsErrShareNotFound/ErrShareLinkExpired. Actual: step 4 returns a signed, downloadable URL.
Impact
A former share recipient (including an anonymous one, under default permissions) can keep minting signed download URLs for already-known shared files for up to 300 s after the owner deletes the share or after time/download-limit expiry, plus the validity window of each signed URL minted in that period. It defeats owner revocation, time expiry, and the remaining-download limit, and bypasses password revalidation on cached state.
Remediation
- On
RestoreState, re-runinventory.IsValidShareand re-compare the currentshare.Passwordbefore trusting cachedshareRoot; or bind the cached state to an authorization version that changes on any share edit/delete/download-limit change. - Do not store authorization-sensitive share state in context-hint cache; treat the hint as a pagination/perf token only.
- Invalidate
navigator_state_*entries when a share is edited or deleted. - Treat
HookTypeBeforeDownloadfailures as blocking for share-backed downloads. - Add a regression test: list + prewarm hint, delete share, then
file/urlwith the same hint must fail.
Références
Vulnérabilités liées
Tout Supply chain →- MEDIUMCVE-2026-72792
SiYuan: Tag labels from password-protected documents are returned to readers who have not entered the password
- MEDIUMCVE-2026-63733
SurrealDB: Writes in a PERMISSIONS clause bypass table permissions
- HIGHGHSA-w8wf-3qvj-6xqf
OpenClaw Feishu permission tools could ignore per-account disablement
- HIGHGHSA-2q7j-2vhx-56g8
OpenClaw Feishu tools could ignore per-account disablement
- MEDIUMCVE-2026-56743
Cilium may unexpectedly allow ingress traffic from the local namespace when a Kubernetes NetworkPolicy is configured with an ipBlock match
- HIGHCVE-2026-73841
OpenChoreo: Cross-project command execution and wirelog view access via OpenChoreo openchoreo-api exec and wirelogs endpoints