GHSA-2wfh-rcwf-wh23 Budibase: Path traversal in plugin file upload enables arbitrary directory deletion and file write
SummaryThe 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 serviceSeverity RationaleDespite 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)DescriptionUnsanitized filename flows into filesystem operationsIn `packages/server/src/api/controllers/plugin/file.ts`, the uploaded file's name is used directly after stripping the `.tar.gz` suffix: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: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 gapThe codebase is aware of the risk in similar paths:**Safe path in `utils.ts`**: The `downloadUnzipTarball` function (for NPM/GitHub/URL plugin sources) generates a random name server-side:This is safe because `name` never contains user input.**Safe path in `objectStore.ts`**: Other uses of `budibaseTempDir()` use UUID-generated names:**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 chainAuthenticated Global Builder sends `POST /api/plugin/upload` with a multipart file whose `Content-Disposition` filename contains path traversal (e.g., `../../etc/target.tar.gz`)koa-body/formidable parses the upload, setting `file.name` to the raw filename from the header`controller.upload` → `sdk.plugins.processUploaded()` → `fileUpload(file)``.endsWith(".tar.gz")` check passes (the suffix is present)`.split(".tar.gz")[0]` extracts `../../etc/target``createTempFolder("../../etc/target")` is called`path.join("/tmp/.budibase", "../../etc/target")` resolves to `/etc/target``fs.rmSync("/etc/target", { recursive: true, force: true })` — **deletes the target directory recursively**`fs.mkdirSync("/etc/target")` — **creates a directory at the traversed path**`extractTarball(file.path, "/etc/target")` — **extracts attacker-controlled tarball contents to the traversed path**Proof of ConceptImpact**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 restartsRecommended RemediationOption 1: Sanitize at `createTempFolder` (preferred — protects all callers)Option 2: Sanitize at the upload handler (defense-in-depth)Strip path components from the filename before use:Both options should ideally be applied together for defense-in-depth.CreditThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).
CVSS v3.1 score: 8.7