CVE-2026-50132: Budibase: Chat Identity Link Hijacking via Missing Consent & CSRF — Account Impersonation in Budibase
Title
Chat Identity Link Hijacking — Attacker Can Silently Map Their Slack/Discord Identity to Any Authenticated Budibase User's Account
Severity
High — CVSS 3.1: AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N = 7.3
Affected Product
- Product: Budibase - Version: 3.37.2 (introduced in this version) - Component: packages/server/src/api/controllers/ai/chatIdentityLinks.ts - Endpoint: GET /api/chat-links/:instance/:token/handoff
Vulnerability Type
- CWE-352: Cross-Site Request Forgery - CWE-284: Improper Access Control
---
Vulnerability Description
GET /api/chat-links/:instance/:token/handoff is a public endpoint (no auth required) that performs a permanent, state-changing operation: it binds an external chat identity (Slack/Discord/MS Teams) to an authenticated Budibase user account, with no consent UI and no CSRF protection.
The session token in the URL is created by the attacker (from their own /link slash command) and embeds the attacker's externalUserId. When an authenticated Budibase victim visits the URL, their account is silently and permanently linked to the attacker's Slack/Discord identity. The server responds with "Authentication succeeded." — no indication of what was linked.
Route Registration
typescript // packages/server/src/api/routes/chat.ts:22 router.get( "/api/chat-links/:instance/:token/handoff", controller.handoffChatLinkSession // registered in publicRoutes — zero auth middleware )
Vulnerable Controller (full function)
typescript // packages/server/src/api/controllers/ai/chatIdentityLinks.ts:61–110 export async function handoffChatLinkSession( ctx: UserCtx<void, string, { instance: string; token: string }> ) { const token = resolveToken(ctx.params.token) const session = await sdk.ai.chatIdentityLinks.getChatIdentityLinkSession(token) if (!session) { throw new HTTPError("Link token is invalid or has expired", 400) } assertSessionMatchesInstance({ workspaceId: session.workspaceId, instance: ctx.params.instance })
if (!ctx.isAuthenticated) { // Unauthenticated: set return URL cookie, redirect to login // After login, same URL is visited again → attack completes silently utils.setCookie(ctx, /api/chat-links/${ctx.params.instance}/${token}/handoff, "budibase:returnurl", { sign: false } // ← unsigned cookie, but not an open redirect ) ctx.redirect("/builder/auth/login") return }
const currentGlobalUserId = getCurrentGlobalUserId(ctx) const consumedSession = await sdk.ai.chatIdentityLinks.consumeChatIdentityLinkSession(token)
// ↓↓↓ THE VULNERABLE WRITE — no consent check, no CSRF token ↓↓↓ await sdk.ai.chatIdentityLinks.upsertChatIdentityLink({ provider: consumedSession.provider, externalUserId: consumedSession.externalUserId, // ← ATTACKER's Slack ID externalUserName: consumedSession.externalUserName, teamId: consumedSession.teamId, globalUserId: currentGlobalUserId, // ← VICTIM's Budibase user ID linkedBy: currentGlobalUserId, })
ctx.type = "text/html" ctx.body = renderLinkSuccessPage() // ← "Authentication succeeded." — no disclosure to user }
---
Proof of Concept — Annotated HTTP Trace
Setup
| Role | Identity | |---|---| | Attacker | Slack user UATTACKER (e.g. UA12345678), Budibase tenant acme, workspace ID wsabc123 | | Victim | Budibase admin, session cookie budibase:session=VICTIMSESSION |
---
Step 1 — Attacker triggers /link in Slack
Attacker types /link to the Budibase Slack bot. Budibase server creates a Redis session:
Redis key: chatIdentityLinkSession:tokxxxxxxxxxxxxxxxx
Redis value (exact structure from ChatIdentityLinkSession interface): json { "token": "tokxxxxxxxxxxxxxxxx", "tenantId": "acme", "workspaceId": "wsabc123", "provider": "slack", "externalUserId": "UA12345678", "externalUserName": "attacker", "teamId": "TACMESLACK", "createdAt": "2026-05-02T10:00:00.000Z", "expiresAt": "2026-05-02T10:10:00.000Z" }
Slack DM sent privately to attacker: Link your Slack account to continue chatting with this agent. https://budibase.company.com/api/chat-links/wsabc123/tokxxxxxxxxxxxxxxxx/handoff
Key observation: This URL embeds the attacker's own externalUserId inside the token. The attacker has full control over which identity gets linked.
---
Step 2 — Attacker forwards URL to victim
Attacker posts in the company Slack: @admin please click this to connect your Budibase account for AI agent access: https://budibase.company.com/api/chat-links/wsabc123/tokxxxxxxxxxxxxxxxx/handoff
---
Step 3 — Victim clicks link (authenticated)
HTTP Request (victim's browser): http GET /api/chat-links/wsabc123/tokxxxxxxxxxxxxxxxx/handoff HTTP/1.1 Host: budibase.company.com Cookie: budibase:session=VICTIMSESSION
HTTP Response: http HTTP/1.1 200 OK Content-Type: text/html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Authentication succeeded</title> </head> <body> <p>Authentication succeeded.</p> <script> if (window.opener && !window.opener.closed) { try { window.opener.focus(); window.close() } catch (error) {} } </script> </body> </html>
The victim sees "Authentication succeeded." with no mention of Slack, no mention of attacker, no mention of what capabilities were granted.
CouchDB global-db document written immediately after (exact structure from upsertChatIdentityLink):
json { "id": "chatidentitylinkacmeslackTACMESLACKUA12345678", "tenantId": "acme", "provider": "slack", "externalUserId": "UA12345678", "globalUserId": "roglobalusVICTIMADMINID", "linkedAt": "2026-05-02T10:00:42.000Z", "linkedBy": "roglobalusVICTIMADMINID", "externalUserName": "attacker", "teamId": "TACMESLACK", "createdAt": "2026-05-02T10:00:42.000Z", "updatedAt": "2026-05-02T10:00:42.000Z" }
The mapping is now permanent. externalUserId = UA12345678 (attacker) → globalUserId = roglobalusVICTIMADMINID (victim).
---
Step 4 — Attacker impersonates victim via AI agent
Attacker sends any message to the Budibase Slack bot from their own account (UA12345678).
The chat handler resolves the identity:
typescript // packages/server/src/api/controllers/webhook/chatHandler.ts:421 const existingLink = await sdk.ai.chatIdentityLinks.getChatIdentityLink({ provider: AgentChannelProvider.SLACK, externalUserId: "UA12345678", // ← attacker's Slack ID teamId: "TACMESLACK", }) // existingLink.globalUserId = "roglobalusVICTIMADMINID"
const linkedUser = await getGlobalUser("roglobalusVICTIMADMINID") // All agent tool calls now execute with victim admin's permissions
The attacker can now ask the agent:
> "Show me all rows in the Customers table" > "Trigger the 'Send Invoice' automation for customer ID 42" > "What files are in the knowledge base?"
Each request runs with the victim admin's identity and permissions. The victim has no indication this is happening.
---
Step 3b — Variant: Victim Not Yet Authenticated
If the victim is not currently logged in when they click the URL:
HTTP Request: http GET /api/chat-links/wsabc123/tokxxxxxxxxxxxxxxxx/handoff HTTP/1.1 Host: budibase.company.com
HTTP Response: http HTTP/1.1 302 Found Location: /builder/auth/login Set-Cookie: budibase:returnurl=%2Fapi%2Fchat-links%2Fwsabc123%2Ftokxxxxxxxxxxxxxxxx%2Fhandoff; Path=/
After the victim logs in, the browser follows the return URL and the attack completes identically to Step 3.
---
Impact
| Dimension | Detail | |---|---| | Confidentiality | High — attacker reads all table rows, files, and knowledge base data accessible to victim | | Integrity | High — attacker writes rows and triggers automations (email, external API calls, record creation) as victim | | Availability | None | | Auth required | Low — attacker only needs a Slack/Discord account in the same workspace as the Budibase bot | | User interaction | Required — victim clicks one link (trivial social engineering in any enterprise Slack) | | Scope | Unchanged — impact is within the victim's Budibase tenant | | Persistence | Permanent — the link document persists in CouchDB until explicitly deleted; re-exploitation survives token rotation |
---
Why Severity Is High (Not Medium)
The social engineering bar is near zero in enterprise Slack: - The link looks like a legitimate Budibase URL on the company domain - The message pattern ("link your account for AI agent access") matches the product's own UX - A victim who clicks and sees "Authentication succeeded." has no reason to be suspicious - The effect is permanent and silent — the victim never learns their account was linked
Combined with admin-level access to all application data and automation triggers, this meets the bar for High.
---
Remediation
Minimum Fix — Add Consent Page
Convert the handoff to a two-step flow:
GET /api/chat-links/:instance/:token/handoff → Show consent page: "You are linking your Budibase account to [externalUserName]'s Slack identity ([provider]). This allows them to interact with AI agents as you. [Confirm] [Cancel]"
POST /api/chat-links/:instance/:token/handoff (with CSRF token) → Perform the upsertChatIdentityLink() write
Moving the write to POST removes it from publicRoutes, making Budibase's existing CSRF middleware apply automatically.
Additional Hardening
- Show the externalUserName and provider on the consent page - Log the event to the audit trail (both identities, timestamp, IP) - Optionally restrict linking to users with explicit permission (not all roles)
--- Credits, Vishal Kumar B https://github.com/VishaaLlKumaaRr
References
- packages/server/src/api/routes/chat.ts:22 — public route registration - packages/server/src/api/controllers/ai/chatIdentityLinks.ts:61–110 — full vulnerable controller - packages/server/src/sdk/workspace/ai/chatIdentityLinks.ts:135–165 — session creation (embeds attacker's externalUserId) - packages/server/src/sdk/workspace/ai/chatIdentityLinks.ts:202–247 — upsertChatIdentityLink (permanent write) - packages/server/src/api/controllers/webhook/chatHandler.ts:421 — identity resolution during agent message handling - packages/server/src/ai/tools/budibase/automations.ts — automation trigger capability - packages/server/src/ai/tools/budibase/rows.ts — row read/write capability - packages/types/src/sdk/chatIdentityLinks.ts — session + link type definitions - CWE-352: Cross-Site Request Forgery - CWE-284: Improper Access Control
Other sources
Budibase is an open-source low-code platform. Prior to 3.39.0, GET /api/chat-links/:instance/:token/handoff is a public endpoint (no auth required) that performs a permanent, state-changing operation: it binds an external chat identity (Slack/Discord/MS Teams) to an authenticated Budibase user account, with no consent UI and no CSRF protection. The session token in the URL is created by the attacker (from their own /link slash command) and embeds the attacker's externalUserId. When an authenticated Budibase victim visits the URL, their account is silently and permanently linked to the attacker's Slack/Discord identity. The server responds with "Authentication succeeded." — no indication of what was linked. This vulnerability is fixed in 3.39.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/@budibase/serverto a version that resolves this vulnerability.Fixed in 3.39.0 - Upgrade
Upgrade
Budibaseto a version that resolves this vulnerability.Fixed in 3.39.0
Event History
Frequently Asked Questions
What is the severity of CVE-2026-50132?
The severity of CVE-2026-50132 is high, with a CVSS score of 7.3.
How do I fix CVE-2026-50132?
To mitigate CVE-2026-50132, upgrade to Budibase version 3.37.3 or later.
What type of vulnerability is CVE-2026-50132?
CVE-2026-50132 is categorized as a Cross-Site Request Forgery (CSRF) vulnerability.
What can an attacker do with CVE-2026-50132?
An attacker can silently hijack a Budibase user's account by mapping their Slack or Discord identity to it.
Which versions of Budibase are affected by CVE-2026-50132?
Budibase version 3.37.2 is affected by CVE-2026-50132.