← All advisories
CVE-2026-54562Medium · CVSS 6.5· CWE-918

Non-admin remote download users can SSRF loopback/internal services and read imported responses

Vendor
cloudreve
Product
Status
Published · Jul 08 2026
Researchers
riodrwn
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Published
Jul 08 2026

Summary

Cloudreve's offline ("remote") download feature passes a fully user-controlled URL (src) to the configured aria2 node with no scheme validation and no internal-address filtering. aria2 fetches the URL and Cloudreve then copies the downloaded response body into the requesting user's storage as a normal file the user can read. An authenticated user whose group has the RemoteDownload permission can therefore make the Cloudreve download node issue arbitrary HTTP(S)/FTP requests to loopback, link-local, and RFC1918 targets and read back the responses — a full-read (not blind) server-side request forgery.

I verified the complete request → fetch → store chain against the source at the audited commit, and reproduced the downloader leg (aria2 addUri fetching an internal-only loopback service and writing its response body to disk) in a sandbox.

Technical detail

1. The source URL is never validated before being queued

service/explorer/workflows.go:81CreateDownloadTask checks only the group permission and the destination; the src URLs are passed straight through:

// :88  — only the permission is checked
if !user.Edges.Group.Permissions.Enabled(int(types.GroupPermissionRemoteDownload)) {
    return nil, serializer.NewError(serializer.CodeGroupNotAllowed, ...)
}
// :108 — only the destination URI is validated (where the file is stored)
_, err = m.Get(c, dst, dbfs.WithRequiredCapabilities(dbfs.NavigatorCapabilityCreateFile))
...
// :135-140 — each user-supplied src is queued with no inspection of its scheme or host
for _, src := range service.Src {
    if src == "" { continue }
    t, err := workflows.NewRemoteDownloadTask(c, src, service.SrcFile, service.Dst)
    ...
}

The request struct confirms src carries no validation tag (unlike dst):

// :74-76
Src     []string `json:"src"`               // no binding/validation
SrcFile string   `json:"src_file"`
Dst     string   `json:"dst" binding:"required"`

2. The raw URL flows untouched to aria2's addUri

pkg/filemanager/workflows/remote_download.go:86 stores the user string verbatim (SrcUri: src), and :203 hands it to the downloader:

// :178
torrentUrl := m.state.SrcUri                 // == the raw user URL
...
// :203
handle, err := m.d.CreateTask(ctx, torrentUrl, user.Edges.Group.Settings.RemoteDownloadOptions)

pkg/downloader/aria2/aria2.go:80 passes it to aria2 with no host check:

gid, err := caller.AddURI(url, downloadOptions)   // url = attacker-controlled

aria2 accepts http, https, and ftp URIs. There is no allowlist/denylist, no scheme restriction, and no rejection of loopback/link-local/RFC1918 hosts at any point in this path.

3. The fetched response body is copied into the user's storage

After aria2 saves the response to the node's temp directory, masterTransfer (remote_download.go:435-478) opens each downloaded file and writes it into the destination the user supplied:

dst := dstUri.JoinRaw(sanitizedName)         // e.g. cr://my/iam-credentials
src := filepath.FromSlash(path.Join(m.state.Status.SavePath, file.Name))
fileStream, err := os.Open(src)
...
_, err = fm.Update(ctx, &fs.UploadRequest{ Props: &fs.UploadProps{Uri: dst, ...}, File: fileStream }, ...)

The internal response is now a regular file in the attacker's cr://my/ storage, readable via the normal file-content download endpoint. This is what makes the finding a read SSRF rather than blind.

4. The generic HTTP client shares the gap

pkg/request/request.go:96-196 builds and sends requests with http.NewRequestWithContextclient.Do(req) and no DialContext/Control hook to block private destinations. Any other code path that reaches this client with a user-influenced target inherits the same exposure.

A codebase-wide search for SSRF/private-IP filtering (ssrf|isPrivate|169\.254|loopback|link-local|rfc1918 etc.) returns no relevant guard; the IsPrivate matches all refer to storage-policy privacy, not address filtering.