Skip to content

Commit a0be07a

Browse files
1 parent fb17b77 commit a0be07a

4 files changed

Lines changed: 268 additions & 0 deletions

File tree

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-2xgg-r2wc-c5r2",
4+
"modified": "2026-07-24T21:26:19Z",
5+
"published": "2026-07-24T21:26:19Z",
6+
"aliases": [],
7+
"summary": "Budibase: MySQL DESCRIBE Backtick Injection via multipleStatements in Database Connector",
8+
"details": "### Summary\n**This is a related but independently fixable vulnerability to GHSA-qqf5-x7mj-v43p\n(PostgreSQL SQL injection), reported in the same original disclosure and\nsplit per GitHub CNA guidance (rule 4.2.11) since it affects a separate\nintegration, has a distinct attack precondition, and requires a separate\npatch.**\n\nThe MySQL integration enables `multipleStatements: true` on the connection,\npermitting semicolon-separated multi-statement execution. During table\nintrospection, table names retrieved from `INFORMATION_SCHEMA.TABLES` are\ninterpolated into a `DESCRIBE` query wrapped in backticks, but embedded\nbackticks in the table name are never escaped — allowing a malicious table\nname to break out and inject a second, attacker-controlled statement.\n\n### Details\n\n**Vulnerable Code:**\nFile: `packages/server/src/integrations/mysql.ts`, lines 172, 305\n\n```typescript\nthis.config = { ...config, multipleStatements: true, ... } // line 172\n...\n{ sql: `DESCRIBE \\`${tableName}\\`;` } // line 305 — backtick NOT escaped\n```\n\nBecause `multipleStatements` is enabled, any statement appended after the\nbacktick break-out executes as a second query in the same round trip.\n\n### Step-by-Step Reproduction\n1. An attacker with the ability to create tables in the target MySQL\n database (e.g. a lower-privileged database user, or a malicious actor in\n a multi-tenant database) creates a table named:\n ``foo`; DROP TABLE users; --``\n2. In Budibase, an administrator triggers schema introspection for that\n database (e.g. opening the datasource or refreshing its table list).\n3. Budibase reads the malicious table name from `INFORMATION_SCHEMA.TABLES`\n and interpolates it into the `DESCRIBE` query.\n4. The unescaped backtick terminates the identifier early, and the\n semicolon-separated payload (enabled by `multipleStatements: true`)\n executes as a second statement.\n\n### Impact\nArbitrary SQL execution triggered during routine schema discovery. Unlike\nthe PostgreSQL and MS SQL Server findings, this does not require the\nattacker to control the Budibase datasource configuration directly — only\nthe ability to create a maliciously named table in the underlying database\nbeforehand, with an administrator's normal use of the introspection feature\nserving as the trigger.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:H/PR:H/UI:R/S:C/C:H/I:H/A:H"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "npm",
19+
"name": "@budibase/server"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "3.38.1"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/Budibase/budibase/security/advisories/GHSA-2xgg-r2wc-c5r2"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/Budibase/budibase/pull/18989"
44+
},
45+
{
46+
"type": "WEB",
47+
"url": "https://github.com/Budibase/budibase/commit/2c61f389c9986c91ddd8ae161c2b5e8ec21c60ac"
48+
},
49+
{
50+
"type": "PACKAGE",
51+
"url": "https://github.com/Budibase/budibase"
52+
},
53+
{
54+
"type": "WEB",
55+
"url": "https://github.com/Budibase/budibase/releases/tag/3.39.18"
56+
}
57+
],
58+
"database_specific": {
59+
"cwe_ids": [
60+
"CWE-89"
61+
],
62+
"severity": "HIGH",
63+
"github_reviewed": true,
64+
"github_reviewed_at": "2026-07-24T21:26:19Z",
65+
"nvd_published_at": null
66+
}
67+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-gh4h-34gr-87r7",
4+
"modified": "2026-07-24T21:25:20Z",
5+
"published": "2026-07-24T21:25:20Z",
6+
"aliases": [],
7+
"summary": " Budibase: OAuth2 Token Disclosure via Automation Test Results Broadcast to Other Builders",
8+
"details": "## Summary\n\nWhen an SSO-authenticated user tests an automation in the Budibase builder, their OAuth2 access token and refresh token are included in the automation test results. These results are broadcast via WebSocket to all builders connected to the same dev app and stored in an in-memory cache accessible to any builder who polls the test status endpoint. This allows any co-builder of the same app to steal the testing user's OAuth2 tokens.\n\n## Details\n\nThe vulnerability exists because `getUserContextBindings()` intentionally includes OAuth2 tokens in user context bindings (so automations can call external APIs), but the automation test pipeline exposes the full result — including these tokens — to all builders of the same app without sanitization.\n\n**Step 1: Tokens included in user bindings**\n\nIn `packages/server/src/sdk/users/utils.ts:134-161`:\n```typescript\nexport function getUserContextBindings(user: ContextUser): UserBindings {\n const bindings: UserBindings = {\n _id: user._id,\n email: user.email,\n // ...\n }\n if (isSSOUser(user) && user.oauth2) {\n bindings.oauth2 = {\n accessToken: user.oauth2.accessToken, // <-- sensitive\n refreshToken: user.oauth2.refreshToken, // <-- sensitive\n }\n }\n return bindings\n}\n```\n\n**Step 2: Bindings passed to automation execution**\n\nIn `packages/server/src/api/controllers/automation.ts:311-312`:\n```typescript\nconst user = sdk.users.getUserContextBindings(ctx.user)\nreturn await triggers.externalTrigger(\n { ...automation, disabled: false },\n { ...input, appId, user }, // user with tokens passed as event param\n { getResponses: true, onProgress: emitProgress }\n)\n```\n\n**Step 3: Tokens placed in trigger outputs**\n\nIn `packages/server/src/threads/automation.ts:409-413`:\n```typescript\nconst trigger: AutomationTriggerResult = {\n id: data.automation.definition.trigger.id,\n stepId: data.automation.definition.trigger.stepId,\n inputs: null,\n outputs: data.event, // data.event includes user.oauth2 tokens\n}\n```\n\n**Step 4: Result broadcast without sanitization**\n\nThe full result (including `trigger.outputs.user.oauth2`) is exposed via two vectors:\n\n1. **WebSocket broadcast** — `builderSocket.emitToRoom()` calls `this.io.in(room).emit()` (`packages/server/src/websockets/websocket.ts:291`) which sends to ALL sockets in the app's room, not just the originator.\n\n2. **Test status endpoint** — `recordTestProgress()` stores the result in a Map keyed by `${appId}:${automationId}` with no user isolation (`packages/server/src/automations/testProgress.ts:41-74`). Any builder can call `GET /api/automations/:id/test/status` to retrieve another user's test results.\n\n## PoC\n\nRequires two builder-level users on the same Budibase app, where User A authenticates via SSO/OIDC (Google, Azure AD, etc.) which provides OAuth2 tokens.\n\n```bash\n# Step 1: User A (SSO-authenticated builder) tests an automation asynchronously\ncurl -X POST http://localhost:10000/api/automations/<automation-id>/test?async=true \\\n -H 'x-budibase-app-id: app_dev_<appid>' \\\n -H 'Cookie: budibase:auth=<userA_session>' \\\n -H 'Content-Type: application/json' \\\n -d '{\"row\": {\"tableId\": \"ta_xxx\"}}'\n# Returns: {\"message\": \"Automation test started\"}\n\n# Step 2: User B (another builder on the same app) polls the test status\ncurl -X GET http://localhost:10000/api/automations/<automation-id>/test/status \\\n -H 'x-budibase-app-id: app_dev_<appid>' \\\n -H 'Cookie: budibase:auth=<userB_session>'\n\n# Response includes the full automation result with:\n# result.trigger.outputs.user.oauth2.accessToken = \"ya29.a0AfH6SM...\"\n# result.trigger.outputs.user.oauth2.refreshToken = \"1//0eXyz...\"\n```\n\nAdditionally, User B can passively receive the tokens by simply having the Budibase builder open (connected via WebSocket), as the `BuilderSocketEvent.AutomationTestProgress` event with `status: \"complete\"` includes the full result payload.\n\n## Impact\n\n- **OAuth2 access tokens** for external services (Google Workspace, Azure AD, GitHub, etc.) are exposed to co-builders of the same app. These tokens can be used to access external APIs as the victim user.\n- **OAuth2 refresh tokens** provide persistent access — an attacker can generate new access tokens even after the original expires, maintaining long-term access to the victim's external service accounts.\n- The attack is passive via WebSocket — an attacker only needs to have the builder UI open to receive tokens when any co-builder tests an automation.\n- Test results persist in memory for 5 minutes (TTL in `testProgress.ts:15`), providing a window for polling-based attacks.\n\n## Recommended Fix\n\nStrip OAuth2 tokens from automation test results before storing/broadcasting them. The tokens are needed during automation execution but should not be included in the result sent to clients.\n\nIn `packages/server/src/api/controllers/automation.ts`, sanitize the result before passing to `emitProgress`:\n\n```typescript\nfunction sanitizeAutomationResult(result: AutomationResults): AutomationResults {\n const sanitized = cloneDeep(result)\n if (sanitized.trigger?.outputs?.user?.oauth2) {\n delete sanitized.trigger.outputs.user.oauth2\n }\n for (const step of sanitized.steps || []) {\n if (step.outputs?.user?.oauth2) {\n delete step.outputs.user.oauth2\n }\n }\n return sanitized\n}\n```\n\nApply sanitization in the `emitProgress` callback at line 290 and before returning `ctx.body` at line 342:\n\n```typescript\nemitProgress({\n status: \"complete\",\n occurredAt: Date.now(),\n result: sanitizeAutomationResult(result),\n})\n```\n\nAdditionally, the `testProgress` store should be scoped per-user (keyed by `${appId}:${automationId}:${userId}`) so that one builder's test results are not accessible to another builder via the `testStatus` endpoint.",
9+
"severity": [
10+
{
11+
"type": "CVSS_V3",
12+
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:N/A:N"
13+
}
14+
],
15+
"affected": [
16+
{
17+
"package": {
18+
"ecosystem": "npm",
19+
"name": "@budibase/server"
20+
},
21+
"ranges": [
22+
{
23+
"type": "ECOSYSTEM",
24+
"events": [
25+
{
26+
"introduced": "0"
27+
},
28+
{
29+
"last_affected": "3.38.1"
30+
}
31+
]
32+
}
33+
]
34+
}
35+
],
36+
"references": [
37+
{
38+
"type": "WEB",
39+
"url": "https://github.com/Budibase/budibase/security/advisories/GHSA-gh4h-34gr-87r7"
40+
},
41+
{
42+
"type": "WEB",
43+
"url": "https://github.com/Budibase/budibase/pull/19107"
44+
},
45+
{
46+
"type": "WEB",
47+
"url": "https://github.com/Budibase/budibase/commit/bca426de7dc36d680285295655dc640dea2aab21"
48+
},
49+
{
50+
"type": "PACKAGE",
51+
"url": "https://github.com/Budibase/budibase"
52+
},
53+
{
54+
"type": "WEB",
55+
"url": "https://github.com/Budibase/budibase/releases/tag/3.39.25"
56+
}
57+
],
58+
"database_specific": {
59+
"cwe_ids": [
60+
"CWE-200"
61+
],
62+
"severity": "MODERATE",
63+
"github_reviewed": true,
64+
"github_reviewed_at": "2026-07-24T21:25:20Z",
65+
"nvd_published_at": null
66+
}
67+
}

0 commit comments

Comments
 (0)