Skip to content

Commit c1bf673

Browse files
1 parent c67fca6 commit c1bf673

3 files changed

Lines changed: 204 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-2wfh-rcwf-wh23",
4+
"modified": "2026-04-04T06:04:19Z",
5+
"published": "2026-04-04T06:04:19Z",
6+
"aliases": [
7+
"CVE-2026-35214"
8+
],
9+
"summary": "Budibase: Path traversal in plugin file upload enables arbitrary directory deletion and file write",
10+
"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).",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:N/I:H/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "@budibase/server"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "3.33.4"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/Budibase/budibase/security/advisories/GHSA-2wfh-rcwf-wh23"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-35214"
46+
},
47+
{
48+
"type": "WEB",
49+
"url": "https://github.com/Budibase/budibase/pull/18240"
50+
},
51+
{
52+
"type": "WEB",
53+
"url": "https://github.com/Budibase/budibase/commit/6344d06d703660fd05995e61d581593c2349c879"
54+
},
55+
{
56+
"type": "PACKAGE",
57+
"url": "https://github.com/Budibase/budibase"
58+
},
59+
{
60+
"type": "WEB",
61+
"url": "https://github.com/Budibase/budibase/releases/tag/3.33.4"
62+
}
63+
],
64+
"database_specific": {
65+
"cwe_ids": [
66+
"CWE-22"
67+
],
68+
"severity": "HIGH",
69+
"github_reviewed": true,
70+
"github_reviewed_at": "2026-04-04T06:04:19Z",
71+
"nvd_published_at": "2026-04-03T16:16:41Z"
72+
}
73+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-6p2j-742g-835f",
4+
"modified": "2026-04-04T06:03:06Z",
5+
"published": "2026-04-04T06:03:06Z",
6+
"aliases": [],
7+
"summary": "actions-mkdocs: Command Injection via issue title in internal GitHub Actions workflow",
8+
"details": "### Summary\n\nExternal input from `github.event.issue.title` is used unsafely in a shell command in `.github/workflows/release-candidate.yaml`, allowing command injection during workflow execution.\n\n### Details\n\nIn `.github/workflows/release-candidate.yaml`, the issue title is interpolated directly into a shell command:\n\n```\nexport VERSION=$(echo ${{ github.event.issue.title }} | sed -E 's/Release v?([0-9\\.]*)/\\1/g')\n```\n\nBecause the issue title is attacker-controlled and is embedded directly into a shell command, shell metacharacters such as command substitution (`$()`) and command separators (`;`) can be interpreted by the shell.\n\nAlthough the workflow checks that the title starts with `Release `, this condition can still be satisfied by a maliciously crafted input.\n\n### PoC\n\n1. Create or edit an issue with the following title:\n\n ```\n Release v1.2.3 $(whoami)\n ```\n\n2. Trigger the workflow that processes the issue.\n\n3. Observe that the injected command is executed on the runner.\n\nThe workflow logs show that `$(whoami)` is evaluated and its output (`runner`) appears in the command result, confirming that attacker-controlled input is executed within the shell.\n\n<img width=\"633\" height=\"380\" alt=\"스크린샷 2026-03-27 오후 8 33 43\" src=\"https://github.com/user-attachments/assets/90b38dab-8c53-4a13-8302-158ac5acf051\" />\n\n\n### Impact\n\nThis vulnerability allows command injection in the GitHub Actions runner through attacker-controlled issue titles. An attacker may be able to execute arbitrary commands within the context of the affected workflow job.\n\nDepending on the workflow configuration (such as permissions and available secrets), successful exploitation could lead to:\n\n* Unauthorized command execution in the CI environment\n* Misuse of the `GITHUB_TOKEN`\n* Modification of repository state, release artifacts, or other workflow outputs\n\nIf the repository is public and allows untrusted users to create or reopen issues that trigger the workflow, this may be exploitable by external users.\n\nThis issue is limited to the repository's internal workflow configuration and does not directly affect downstream users of the published `actions-mkdocs` GitHub Action.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "GitHub Actions",
19+
"name": "Tiryoh/actions-mkdocs"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"fixed": "0.25.0"
30+
}
31+
]
32+
}
33+
],
34+
"database_specific": {
35+
"last_known_affected_version_range": "<= 0.24.0"
36+
}
37+
}
38+
],
39+
"references": [
40+
{
41+
"type": "WEB",
42+
"url": "https://github.com/Tiryoh/actions-mkdocs/security/advisories/GHSA-6p2j-742g-835f"
43+
},
44+
{
45+
"type": "PACKAGE",
46+
"url": "https://github.com/Tiryoh/actions-mkdocs"
47+
}
48+
],
49+
"database_specific": {
50+
"cwe_ids": [
51+
"CWE-77"
52+
],
53+
"severity": "MODERATE",
54+
"github_reviewed": true,
55+
"github_reviewed_at": "2026-04-04T06:03:06Z",
56+
"nvd_published_at": null
57+
}
58+
}

0 commit comments

Comments
 (0)