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_BUILDERpermission) - 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:
// 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:
// 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:
Safe path in
utils.ts: ThedownloadUnzipTarballfunction (for NPM/GitHub/URL plugin sources) generates a random name server-side:// packages/server/src/api/controllers/plugin/index.ts:68 const name = "PLUGIN_" + Math.floor(100000 + Math.random() * 900000)This is safe because
namenever contains user input.Safe path in
objectStore.ts: Other uses ofbudibaseTempDir()use UUID-generated names:// packages/backend-core/src/objectStore/objectStore.ts:546 const outputPath = join(budibaseTempDir(), v4())Sanitization exists but is not applied: The codebase has
sanitizeKey()inobjectStore.tsfor sanitizing object store paths, but no equivalent is applied tocreateTempFolder's input.
The file upload path is the only caller of createTempFolder that passes unsanitized user input.
Execution chain
- Authenticated Global Builder sends
POST /api/plugin/uploadwith a multipart file whoseContent-Dispositionfilename contains path traversal (e.g.,../../etc/target.tar.gz) - koa-body/formidable parses the upload, setting
file.nameto 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/targetcreateTempFolder("../../etc/target")is calledpath.join("/tmp/.budibase", "../../etc/target")resolves to/etc/targetfs.rmSync("/etc/target", { recursive: true, force: true }), deletes the target directory recursivelyfs.mkdirSync("/etc/target"), creates a directory at the traversed pathextractTarball(file.path, "/etc/target"), extracts attacker-controlled tarball contents to the traversed path
Proof of Concept
# 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
Recommended Remediation
Option 1: Sanitize at createTempFolder (preferred, protects all callers)
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:
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.
Impact
- Arbitrary directory deletion:
rmSyncwith{ 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
Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files. Typical impact: unauthorized file read or write outside the intended directory.
CVE-2026-35214 has a CVSS score of 8.7 (High). The vector is network-reachable, high privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.33.4); upgrading removes the vulnerable code path.
Affected versions
Security releases
Kodem intelligence
Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.
Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.
Remediation advice
Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.
Frequently Asked Questions
- What is CVE-2026-35214? CVE-2026-35214 is a high-severity path traversal vulnerability in @budibase/server (npm), affecting versions < 3.33.4. It is fixed in 3.33.4. Input manipulates file paths to reach files outside the intended directory, such as configuration or credential files.
- How severe is CVE-2026-35214? CVE-2026-35214 has a CVSS score of 8.7 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
- Which versions of @budibase/server are affected by CVE-2026-35214? @budibase/server (npm) versions < 3.33.4 is affected.
- Is there a fix for CVE-2026-35214? Yes. CVE-2026-35214 is fixed in 3.33.4. Upgrade to this version or later.
- Is CVE-2026-35214 exploitable, and should I be worried? Whether CVE-2026-35214 is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
- What actually determines whether CVE-2026-35214 is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
- How do I fix CVE-2026-35214? Upgrade
@budibase/serverto 3.33.4 or later.