CVE-2026-35214: Budibase Path Traversal Vulnerability

HIGH
Advisory Identifier: GHSA-2wfh-rcwf-wh23
CVSS 8.7
CWE-22
Published
Advisory Summary

CVE-2026-35214 is a HIGH (CVSS 8.7) path traversal vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Budibase libraries in the npm ecosystem. The issue involves path traversal in plugin file upload enables arbitrary directory deletion and file write. Remediation is available by upgrading affected packages to version 3.33.4.

CVE-2026-35214 at a Glance

CVE Identifier CVE-2026-35214
GHSA Identifier GHSA-2wfh-rcwf-wh23
Severity HIGH
CVSS v3.1 Base Score 8.7
CWE Classification CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Affected Product Budibase
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 8.7

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required High (PR:H)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality None (C:N)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-35214

To remediate this vulnerability, upgrade all affected Budibase dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@budibase/server fixed: 3.33.4
npm install @budibase/server@3.33.4
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @budibase/server

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-35214, inspect your project dependencies using npm:

npm ls @budibase/server

Analyze the terminal output to locate any version of Budibase in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.33.4), your application is affected.

Note: The npm ls @budibase/server command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where 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. In Budibase, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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).

CVE-2026-35214 Affected Budibase Versions

1 component(s)
Fixed Versions:
3.33.4
Product / Package Ecosystem Affected Versions Fixed Version
@budibase/server
npm
< 3.33.4
3.33.4

References

Frequently Asked Questions

What is CVE-2026-35214?
CVE-2026-35214 is a security vulnerability in Budibase involving Path traversal in plugin file upload enables arbitrary directory deletion and file write under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
Which Budibase versions are affected?
Affected versions include releases prior to 3.33.4 across affected packages: @budibase/server.
What version fixes CVE-2026-35214?
Upgrading to 3.33.4 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @budibase/server affected?
Yes, @budibase/server is affected in releases prior to 3.33.4. Upgrade your dependency to 3.33.4 or later.
What is the CVSS score of CVE-2026-35214?
CVE-2026-35214 has a CVSS v3.1 base score of 8.7 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H.
What CWE is associated with CVE-2026-35214?
CVE-2026-35214 is classified under CWE-22: 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.).
How can I check npm dependencies for CVE-2026-35214?
Run the dependency tree command: 'npm ls @budibase/server'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-2wfh-rcwf-wh23",
  "modified" : "2026-04-04T06:04:19Z",
  "published" : "2026-04-04T06:04:19Z",
  "aliases" : [ "CVE-2026-35214" ],
  "summary" : "Budibase: Path traversal in plugin file upload enables arbitrary directory deletion and file write",
  "details" : "## Summary\n\nThe 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.\n\n## Severity\n\n- **Attack Vector:** Network — exploitable via the plugin upload HTTP API\n- **Attack Complexity:** Low — no special conditions; a single crafted multipart request suffices\n- **Privileges Required:** High — requires Global Builder role (`GLOBAL_BUILDER` permission)\n- **User Interaction:** None\n- **Scope:** Changed — the plugin upload feature is scoped to a temp directory, but the traversal escapes to the host filesystem\n- **Confidentiality Impact:** None — the vulnerability enables deletion and writing, not reading\n- **Integrity Impact:** High — attacker can delete arbitrary directories and write arbitrary files via tarball extraction\n- **Availability Impact:** High — recursive deletion of application or system directories causes denial of service\n\n### Severity Rationale\n\n 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.\n\n## Affected Component\n\n- `packages/server/src/api/controllers/plugin/file.ts` — `fileUpload()` (line 15)\n- `packages/server/src/utilities/fileSystem/filesystem.ts` — `createTempFolder()` (lines 78-91)\n\n## Description\n\n### Unsanitized filename flows into filesystem operations\n\nIn `packages/server/src/api/controllers/plugin/file.ts`, the uploaded file's name is used directly after stripping the `.tar.gz` suffix:\n\n```typescript\n// packages/server/src/api/controllers/plugin/file.ts:8-19\nexport async function fileUpload(file: KoaFile) {\n  if (!file.name || !file.path) {\n    throw new Error(\"File is not valid - cannot upload.\")\n  }\n  if (!file.name.endsWith(\".tar.gz\")) {\n    throw new Error(\"Plugin must be compressed into a gzipped tarball.\")\n  }\n  const path = createTempFolder(file.name.split(\".tar.gz\")[0])\n  await extractTarball(file.path, path)\n\n  return await getPluginMetadata(path)\n}\n```\n\nThe `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.\n\nThe `createTempFolder` function in `packages/server/src/utilities/fileSystem/filesystem.ts` uses `path.join()` which resolves `../` sequences, then performs destructive filesystem operations:\n\n```typescript\n// packages/server/src/utilities/fileSystem/filesystem.ts:78-91\nexport const createTempFolder = (item: string) => {\n  const path = join(budibaseTempDir(), item)\n  try {\n    // remove old tmp directories automatically - don't combine\n    if (fs.existsSync(path)) {\n      fs.rmSync(path, { recursive: true, force: true })\n    }\n    fs.mkdirSync(path)\n  } catch (err: any) {\n    throw new Error(`Path cannot be created: ${err.message}`)\n  }\n\n  return path\n}\n```\n\nThe `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`.\n\n### Inconsistent defenses confirm the gap\n\nThe codebase is aware of the risk in similar paths:\n\n1. **Safe path in `utils.ts`**: The `downloadUnzipTarball` function (for NPM/GitHub/URL plugin sources) generates a random name server-side:\n   ```typescript\n   // packages/server/src/api/controllers/plugin/index.ts:68\n   const name = \"PLUGIN_\" + Math.floor(100000 + Math.random() * 900000)\n   ```\n   This is safe because `name` never contains user input.\n\n2. **Safe path in `objectStore.ts`**: Other uses of `budibaseTempDir()` use UUID-generated names:\n   ```typescript\n   // packages/backend-core/src/objectStore/objectStore.ts:546\n   const outputPath = join(budibaseTempDir(), v4())\n   ```\n\n3. **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.\n\nThe file upload path is the only caller of `createTempFolder` that passes unsanitized user input.\n\n### Execution chain\n\n1. 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`)\n2. koa-body/formidable parses the upload, setting `file.name` to the raw filename from the header\n3. `controller.upload` → `sdk.plugins.processUploaded()` → `fileUpload(file)`\n4. `.endsWith(\".tar.gz\")` check passes (the suffix is present)\n5. `.split(\".tar.gz\")[0]` extracts `../../etc/target`\n6. `createTempFolder(\"../../etc/target\")` is called\n7. `path.join(\"/tmp/.budibase\", \"../../etc/target\")` resolves to `/etc/target`\n8. `fs.rmSync(\"/etc/target\", { recursive: true, force: true })` — **deletes the target directory recursively**\n9. `fs.mkdirSync(\"/etc/target\")` — **creates a directory at the traversed path**\n10. `extractTarball(file.path, \"/etc/target\")` — **extracts attacker-controlled tarball contents to the traversed path**\n\n## Proof of Concept\n\n```bash\n# Create a minimal tarball with a test file\nmkdir -p /tmp/plugin-poc && echo \"pwned\" > /tmp/plugin-poc/test.txt\ntar czf /tmp/poc-plugin.tar.gz -C /tmp/plugin-poc .\n\n# Upload with a traversal filename targeting /tmp/pwned (non-destructive demo)\ncurl -X POST 'http://localhost:10000/api/plugin/upload' \\\n  -H 'Cookie: <global_builder_session_cookie>' \\\n  -F \"file=@/tmp/poc-plugin.tar.gz;filename=../../tmp/pwned.tar.gz\"\n\n# Result: server executes:\n#   rm -rf /tmp/pwned        (if exists)\n#   mkdir /tmp/pwned\n#   tar xzf <upload> -C /tmp/pwned\n# Verify: ls /tmp/pwned/test.txt\n```\n\n## Impact\n\n- **Arbitrary directory deletion**: `rmSync` with `{ recursive: true, force: true }` deletes any directory the Node.js process can access, including application data directories\n- **Arbitrary file write**: Tarball extraction writes attacker-controlled files to any writable path, potentially overwriting application code, configuration, or system files\n- **Denial of service**: Deleting critical directories (e.g., the application's data directory, node_modules, or system directories) crashes the application\n- **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\n\n## Recommended Remediation\n\n### Option 1: Sanitize at `createTempFolder` (preferred — protects all callers)\n\n```typescript\nimport { join, resolve } from \"path\"\n\nexport const createTempFolder = (item: string) => {\n  const tempDir = budibaseTempDir()\n  const resolved = resolve(tempDir, item)\n\n  // Ensure the resolved path is within the temp directory\n  if (!resolved.startsWith(tempDir + \"/\") && resolved !== tempDir) {\n    throw new Error(\"Invalid path: directory traversal detected\")\n  }\n\n  try {\n    if (fs.existsSync(resolved)) {\n      fs.rmSync(resolved, { recursive: true, force: true })\n    }\n    fs.mkdirSync(resolved)\n  } catch (err: any) {\n    throw new Error(`Path cannot be created: ${err.message}`)\n  }\n\n  return resolved\n}\n```\n\n### Option 2: Sanitize at the upload handler (defense-in-depth)\n\nStrip path components from the filename before use:\n\n```typescript\nimport path from \"path\"\n\nexport async function fileUpload(file: KoaFile) {\n  if (!file.name || !file.path) {\n    throw new Error(\"File is not valid - cannot upload.\")\n  }\n  if (!file.name.endsWith(\".tar.gz\")) {\n    throw new Error(\"Plugin must be compressed into a gzipped tarball.\")\n  }\n  // Strip directory components from the filename\n  const safeName = path.basename(file.name).split(\".tar.gz\")[0]\n  const dir = createTempFolder(safeName)\n  await extractTarball(file.path, dir)\n\n  return await getPluginMetadata(dir)\n}\n```\n\nBoth options should ideally be applied together for defense-in-depth.\n\n## Credit\n\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@budibase/server"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.33.4"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/security/advisories/GHSA-2wfh-rcwf-wh23"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-35214"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/pull/18240"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/commit/6344d06d703660fd05995e61d581593c2349c879"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/Budibase/budibase"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/Budibase/budibase/releases/tag/3.33.4"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-22" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-04T06:04:19Z",
    "nvd_published_at" : "2026-04-03T16:16:41Z"
  }
}