GitHub Security Advisory: GHSA-fcm4-4pj2-m5hf
Package: @budibase/server (NPM)
Severity: CRITICAL (CVSS 9.0)
CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Vulnerable versions: < 3.33.4
Patched in: 3.33.4

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

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

#### En