GitHub Security Advisory: GHSA-xh5j-727m-w6gg
Package: budibase (NPM)
Severity: HIGH (CVSS 7.7)
CWE: CWE-918: Server-Side Request Forgery (SSRF)
Vulnerable versions: <= 3.34.11
Patched in: 3.35.10

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

---

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

| At