Budibase: Path traversal in plugin file upload enables arbitrary directory deletion and file write
Published April 4, 2026Updated April 4, 2026Source: osv
Summary
## Summary
The plugin file upload endpoint (`POST /api/plugin/upload`) passes the user-supplied filename directly to `createTempFolder()` without sanitizing path traversal sequences. An attacker with Global Builder privileges can craft a multipart upload with a filename containing `../` to delete arbitrary directories via `rmSync` and write arbitrary files via tarball extraction to any filesystem path the Node.js process can access.
## Severity
- **Attack Vector:** Network — exploitable via the plugin upload HTTP API
- **Attack Complexity:** Low — no special conditions; a single crafted multipart request suffices
- **Privileges Required:** High — requires Global Builder role (`GLOBAL_BUILDER` permission)
- **User Interaction:** None
- **Scope:** Changed — the plugin upload feature is scoped to a temp directory, but the traversal escapes to the host filesystem
- **Confidentiality Impact:** None — the vulnerability enables deletion and writing, not reading
- **Integrity Impact:** High — attacker can delete arbitrary directories and write arbitrary files via tarball extraction
- **Availability Impact:** High — recursive deletion of application or system directories causes denial of service
### Severity Rationale
Despite the real filesystem impact, severity is bounded by the requirement for Global Builder privileges (PR:H), which is the highest non-admin role in Budibase. In self-hosted deployments the Global Builder may already have server access, further reducing practical impact. In cloud/multi-tenant deployments the impact is more significant as it could affect the host infrastructure.
## Affected Component
- `packages/server/src/api/controllers/plugin/file.ts` — `fileUpload()` (line 15)
- `packages/server/src/utilities/fileSystem/filesystem.ts` — `createTempFolder()` (lines 78-91)
## Description
### Unsanitized filename flows into filesystem operations
In `packages/server/src/api/controllers/plugin/file.ts`, the uploaded file's name is used directly after stripping the `.tar.gz` suffix:
```typescript
// packages/server/src/api/controllers/plugin/file.ts:8-19
export async function fileUpload(file: KoaFile) {
if (!file.name || !file.path) {
throw new Error("File is not valid - cannot upload.")
}
if (!file.name.endsWith(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
const path = createTempFolder(file.name.split(".tar.gz")[0])
await extractTarball(file.path, path)
return await getPluginMetadata(path)
}
```
The `file.name` originates from the `Content-Disposition` header's `filename` field in the multipart upload, parsed by formidable (via koa-body 4.2.0). Formidable does not sanitize path traversal sequences from filenames.
The `createTempFolder` function in `packages/server/src/utilities/fileSystem/filesystem.ts` uses `path.join()` which resolves `../` sequences, then performs destructive filesystem operations:
```typescript
// packages/server/src/utilities/fileSystem/filesystem.ts:78-91
export const createTempFolder = (item: string) => {
const path = join(budibaseTempDir(), item)
try {
// remove old tmp directories automatically - don't combine
if (fs.existsSync(path)) {
fs.rmSync(path, { recursive: true, force: true })
}
fs.mkdirSync(path)
} catch (err: any) {
throw new Error(`Path cannot be created: ${err.message}`)
}
return path
}
```
The `budibaseTempDir()` returns `/tmp/.budibase` (from `packages/backend-core/src/objectStore/utils.ts:33`). With a filename like `../../etc/target.tar.gz`, `path.join("/tmp/.budibase", "../../etc/target")` resolves to `/etc/target`.
### Inconsistent defenses confirm the gap
The codebase is aware of the risk in similar paths:
1. **Safe path in `utils.ts`**: The `downloadUnzipTarball` function (for NPM/GitHub/URL plugin sources) generates a random name server-side:
```typescript
// packages/server/src/api/controllers/plugin/index.ts:68
const name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)
```
This is safe because `name` never contains user input.
2. **Safe path in `objectStore.ts`**: Other uses of `budibaseTempDir()` use UUID-generated names:
```typescript
// packages/backend-core/src/objectStore/objectStore.ts:546
const outputPath = join(budibaseTempDir(), v4())
```
3. **Sanitization exists but is not applied**: The codebase has `sanitizeKey()` in `objectStore.ts` for sanitizing object store paths, but no equivalent is applied to `createTempFolder`'s input.
The file upload path is the only caller of `createTempFolder` that passes unsanitized user input.
### Execution chain
1. Authenticated Global Builder sends `POST /api/plugin/upload` with a multipart file whose `Content-Disposition` filename contains path traversal (e.g., `../../etc/target.tar.gz`)
2. koa-body/formidable parses the upload, setting `file.name` to the raw filename from the header
3. `controller.upload` → `sdk.plugins.processUploaded()` → `fileUpload(file)`
4. `.endsWith(".tar.gz")` check passes (the suffix is present)
5. `.split(".tar.gz")[0]` extracts `../../etc/target`
6. `createTempFolder("../../etc/target")` is called
7. `path.join("/tmp/.budibase", "../../etc/target")` resolves to `/etc/target`
8. `fs.rmSync("/etc/target", { recursive: true, force: true })` — **deletes the target directory recursively**
9. `fs.mkdirSync("/etc/target")` — **creates a directory at the traversed path**
10. `extractTarball(file.path, "/etc/target")` — **extracts attacker-controlled tarball contents to the traversed path**
## Proof of Concept
```bash
# Create a minimal tarball with a test file
mkdir -p /tmp/plugin-poc && echo "pwned" > /tmp/plugin-poc/test.txt
tar czf /tmp/poc-plugin.tar.gz -C /tmp/plugin-poc .
# Upload with a traversal filename targeting /tmp/pwned (non-destructive demo)
curl -X POST 'http://localhost:10000/api/plugin/upload' \
-H 'Cookie: <global_builder_session_cookie>' \
-F "file=@/tmp/poc-plugin.tar.gz;filename=../../tmp/pwned.tar.gz"
# Result: server executes:
# rm -rf /tmp/pwned (if exists)
# mkdir /tmp/pwned
# tar xzf <upload> -C /tmp/pwned
# Verify: ls /tmp/pwned/test.txt
```
## Impact
- **Arbitrary directory deletion**: `rmSync` with `{ recursive: true, force: true }` deletes any directory the Node.js process can access, including application data directories
- **Arbitrary file write**: Tarball extraction writes attacker-controlled files to any writable path, potentially overwriting application code, configuration, or system files
- **Denial of service**: Deleting critical directories (e.g., the application's data directory, node_modules, or system directories) crashes the application
- **Potential code execution**: In containerized deployments (common for Budibase) where Node.js runs as root, an attacker could overwrite startup scripts or application code to achieve remote code execution on subsequent restarts
## Recommended Remediation
### Option 1: Sanitize at `createTempFolder` (preferred — protects all callers)
```typescript
import { join, resolve } from "path"
export const createTempFolder = (item: string) => {
const tempDir = budibaseTempDir()
const resolved = resolve(tempDir, item)
// Ensure the resolved path is within the temp directory
if (!resolved.startsWith(tempDir + "/") && resolved !== tempDir) {
throw new Error("Invalid path: directory traversal detected")
}
try {
if (fs.existsSync(resolved)) {
fs.rmSync(resolved, { recursive: true, force: true })
}
fs.mkdirSync(resolved)
} catch (err: any) {
throw new Error(`Path cannot be created: ${err.message}`)
}
return resolved
}
```
### Option 2: Sanitize at the upload handler (defense-in-depth)
Strip path components from the filename before use:
```typescript
import path from "path"
export async function fileUpload(file: KoaFile) {
if (!file.name || !file.path) {
throw new Error("File is not valid - cannot upload.")
}
if (!file.name.endsWith(".tar.gz")) {
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
// Strip directory components from the filename
const safeName = path.basename(file.name).split(".tar.gz")[0]
const dir = createTempFolder(safeName)
await extractTarball(file.path, dir)
return await getPluginMetadata(dir)
}
```
Both options should ideally be applied together for defense-in-depth.
## Credit
This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).
Remediation
Upgrade to the fixed version using your package manager.
npm
Update @budibase/server to 3.33.4 or later
npm install @budibase/server@3.33.4
After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.
Budibase: Path traversal in plugin file upload enables arbitrary directory deletion and file write This vulnerability has been assigned a severity rating of HIGH (CVSS score: 8.7/10).
How do I check if my project is affected by CVE-2026-35214?
CVE-2026-35214 affects @budibase/server. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-35214 and 200,000+ other known vulnerabilities.
Severity & Exploitability
CVSS Score
8.7
High exploitability or significant impact. Prioritize remediation within days.
Budibase: `PUT /api/datasources/:datasourceId` is protected only by `TABLE/READ` permission instead of builder access, allowing any authenticated app user to overwrite datasource connection parameters including host, port, and URL