Skip to content

GHSA-pvcr-8mvp-w8qr CVE-2026-72849

Budibase: Chat-Link Handoff Identity Confusion (Same-Tenant Account-Link CSRF)

High npm
Severity
8.7 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.16%
Chance of exploitation in the next 30 days. Higher than 5.41% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-285 Improper Authorization
CWE-345 Insufficient Verification of Data Authenticity
CWE-352 Cross-Site Request Forgery (CSRF)
Fix
None yet
No fixed version published

Description

Summary

The Budibase AI chat-link handoff flow (GET/POST /api/chat-links/:instance/:token/handoff) binds an external chat identity (Slack/Discord/MS Teams/Telegram) to a Budibase user account. The confirmation endpoint is on a public route (no CSRF middleware, no auth-group gate) and the only credential it checks is a confirmationToken that is already rendered in plaintext into the HTML confirmation page the victim views. There is no binding between the confirmation token and the requester's Budibase session at preparation time, and no CSRF token on the POST.

Consequently, an attacker who creates a chat-link session for their own external chat identity (or any identity they can mint in their chat platform) can induce a victim Budibase user (same tenant) to submit the confirmation POST -> for example by sending them a link that auto-submits, or by XSS/CSRF on a co-tenanted page -> and the victim's globalUserId becomes bound to the attacker's external identity. The attacker then sends messages to the AI agent from their chat platform and is acting as the victim user inside Budibase automations/agent operations, inheriting the victim's permissions on agent operations, knowledge sources, and any downstream automation steps keyed off the linked identity.


Affected

Component Path Lines
Public handoff routes (no auth-group middleware) packages/server/src/api/routes/chat.ts 23 (GET /api/chat-links/:instance/:token/handoff), 27 (POST .../handoff)
Confirmation controller packages/server/src/api/controllers/ai/chatIdentityLinks.ts 124-177 (confirmChatLinkSession); the binding at 153-173
Confirmation token rendered into HTML packages/server/src/api/controllers/ai/chatIdentityLinks.ts 40-60 (renderLinkConfirmationPage); the hidden input at 55
Session creation (attacker side) packages/server/src/sdk/workspace/ai/chatIdentityLinks.ts 138-171 (createChatIdentityLinkSession), 173-188 (prepareChatIdentityLinkSessionConfirmation)
Upsert of identity link packages/server/src/sdk/workspace/ai/chatIdentityLinks.ts 201-250 (upsertChatIdentityLink)

Affected versions: master at commit 3c8d1b4023.

Reachable over HTTP by: the GET and POST are on publicRoutes (no auth-group middleware). The POST requires ctx.isAuthenticated at runtime (controllers/ai/chatIdentityLinks.ts:142) -> so the victim must be logged into Budibase when the POST fires (achievable via standard CSRF if cookies are sent cross-origin, or via phishing that lures the victim to submit).


Root cause

Issue 1 -> The handoff routes are public and unauthenticated at the middleware layer.

// packages/server/src/api/routes/chat.ts:23, 27
publicRoutes.get("/api/chat-links/:instance/:token/handoff", ai.handoffChatLinkSession)
publicRoutes.post("/api/chat-links/:instance/:token/handoff", ai.confirmChatLinkSession)

publicRoutes has no group middleware (endpointGroups/standard.ts:24-25 calls endpointGroupList.group() with no middleware and then .lockMiddleware()). The routes do not have a per-route authorized(...). There is no CSRF middleware on these routes -> Budibase's CSRF synchroniser token is enforced only for state-changing verbs on authenticated routes that are not in NO_CSRF_ENDPOINTS; the public-routes path bypasses it.

Issue 2 -> The confirmation token is exposed to anyone who views the GET page.

// packages/server/src/api/controllers/ai/chatIdentityLinks.ts:40-60
const renderLinkConfirmationPage = (session, action) => {
  ...
  return `<!doctype html>...
    <form method="post" action="${helpers.escapeHtml(action)}">
      <input type="hidden" name="confirmationToken" value="${helpers.escapeHtml(session.confirmationToken)}">
      <button type="submit">Confirm</button>
    </form>
  ...`
}

The confirmationToken (a newid() UUIDv4 generated by prepareChatIdentityLinkSessionConfirmation) is rendered into a hidden form input. Anyone who loads the GET page sees the token in the page source.

Issue 3 -> The POST binds the currently-authenticated user to the external identity based solely on the confirmation token.

// packages/server/src/api/controllers/ai/chatIdentityLinks.ts:124-177
export async function confirmChatLinkSession(ctx) {
  ...
  if (!ctx.isAuthenticated) { throw new HTTPError("Authentication is required to link chat identity", 401) }
  if (!session.confirmationToken || ctx.request.body?.confirmationToken !== session.confirmationToken) {
    throw new HTTPError("Link confirmation is invalid or has expired", 400)
  }

  const currentGlobalUserId = getCurrentGlobalUserId(ctx)        // <- victim's ID
  const consumedSession = await sdk.ai.chatIdentityLinks.consumeChatIdentityLinkSession(token)
  ...
  await sdk.ai.chatIdentityLinks.upsertChatIdentityLink({
    provider: consumedSession.provider,
    externalUserId: consumedSession.externalUserId,              // <- attacker's chat identity
    externalUserName: consumedSession.externalUserName,
    ...
    globalUserId: currentGlobalUserId,                            // <- bound to victim
    linkedBy: currentGlobalUserId,
  })
  ...
}

There is no binding between the session and the requester's Budibase identity at preparation time. prepareChatIdentityLinkSessionConfirmation (sdk/workspace/ai/chatIdentityLinks.ts:173-188) stores the confirmationToken keyed by token with no user-id field. Whoever is authenticated when the POST fires (and supplies the correct confirmationToken) gets bound.

Issue 4 -> assertSessionMatchesInstance only checks workspace ID, not user.

// packages/server/src/api/controllers/ai/chatIdentityLinks.ts:17-27
const assertSessionMatchesInstance = ({ workspaceId, instance }) => {
  if (!workspaceId || workspaceId !== instance) {
    throw new HTTPError("Link token is not valid for this workspace", 400)
  }
}

The check confirms the session belongs to the same workspace as the URL instance param -> useful for preventing cross-workspace confusion but useless against same-tenant identity confusion.


Reproduction

Step-by-step attack

  1. Attacker (tenant T) provisions a chat-link session for their own external identity. Via the agent channel provisioning flow (e.g. POST /api/agent/:agentId/slack/provision or via the Discord/MS Teams/Telegram provisioning endpoints), the attacker obtains a chat-link token bound to their own Slack user ID. The session is stored in Redis keyed by token, scoped to tenant T.

  2. Attacker triggers prepareChatIdentityLinkSessionConfirmation. Either by GETting the handoff page themselves (if they have a Budibase session) or via an internal API. The confirmationToken is generated and stored.

  3. Attacker crafts a phishing/auto-submit page that POSTs to /api/chat-links/<instance>/<token>/handoff with the leaked confirmationToken in the body. Example:

    <form id="f" method="post" action="https://victim.budibase.app/api/chat-links/app_xxx/linktoken/handoff">
      <input name="confirmationToken" value="<leaked-token>">
    </form>
    <script>document.getElementById('f').submit()</script>
    
  4. Victim (a Budibase user in tenant T, e.g. an admin) is lured to the attacker page while logged into Budibase. Their browser sends the POST cross-origin. If the Budibase auth cookie is sent on cross-site requests (SameSite=Lax by default allows top-level POST navigations, which a form-submit is), the request is authenticated as the victim.

  5. The victim's globalUserId is now bound to the attacker's Slack identity. When the attacker DMs the AI agent from Slack, the agent's operations run as the victim user -> with the victim's permissions on agent operations, knowledge sources, file uploads, and any downstream automation steps keyed off the linked identity.

Mitigating factors:

  • SameSite=Lax cookies block cross-site POSTs from sub-resources (fetch / XHR) but allow top-level form-POST navigations. A phishing page that submits the form via document.form.submit() (a top-level navigation) succeeds. So the CSRF works with the default cookie policy.
  • The attack is same-tenant only (session.tenantId !== context.getTenantId() is checked in the sdk).
  • The attacker must induce the victim to click a link (standard phishing). No silent drive-by.

Impact

Capability Available
Bind an attacker-controlled chat identity to a victim's Budibase account ✅
Send messages to the AI agent as the victim user (Slack/Discord/MS Teams/Telegram) ✅
Inherit the victim's permissions on agent operations ✅
Trigger automations / agent operations that the victim is authorised for ✅
Read knowledge sources the victim has access to ✅

The severity depends on what the agent can do as the victim. For an admin victim, this is full tenant administration via chat. For a regular user, it is impersonation within the agent subsystem. The bound identity persists until manually unlinked, so the attacker retains ongoing access.

This is a same-tenant, user-interaction-required primitive. It is below the "unauthenticated RCE" threshold but above "hardening" -> it is a real authentication-confusion vulnerability on a sensitive identity-binding operation.


Fix

Recommended layered fixes:

  1. Bind the confirmation token to the requester's Budibase session at preparation time. In prepareChatIdentityLinkSessionConfirmation, store the globalUserId of the requester alongside the confirmationToken. In confirmChatLinkSession, verify that getCurrentGlobalUserId(ctx) matches the stored requester. This breaks the CSRF / cross-user confusion.

  2. Add a CSRF token to the confirmation POST. The standard Budibase CSRF synchroniser token (x-csrf-token header, validated against the session's csrfToken) should be enforced on POST /api/chat-links/.../handoff. Move the route out of publicRoutes to an authenticated route group so CSRF applies (the GET can remain public for the redirect-to-login flow; the POST should be authenticated + CSRF-protected).

  3. Add a per-session nonce that is bound to the requester's browser (e.g. a signed cookie set on the GET, validated on the POST) to prevent cross-origin submission even when SameSite=Lax allows it.

  4. Require user re-authentication (re-entry of password / step-up auth) for sensitive identity-binding operations, similar to how password change requires re-auth.

Affected versions

npm

PackageVulnerableFixed in
@budibase/server
<= 3.38.1
No fix yet

How to fix

No fixed version has been published yet. Until one is, remove or replace the package, or turn off or isolate the affected feature. This page updates when a fix ships.

Severity breakdown

CVSS 4.0 8.7

CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:P/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Attack vector
Network
Attack complexity
High
Attack requirements
Present
Privileges required
Low
User interaction
Passive
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
High
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
High
Integrity (subsequent systems)
High
Availability (subsequent systems)
None
Exploit maturity
Not defined
Scope
X

CVSS 3.1 7.7

CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:H/I:H/A:N

Attack vector
Network
Attack complexity
High
Privileges required
Low
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None

Weakness

CWE-285: Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.

How to prevent it
  • Architecture and Design: Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) to enforce the roles at the appropriate boundaries. Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
  • Architecture and Design: Ensure that you perform access control checks related to your business logic. These checks may be different than the access control checks that you apply to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor.
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid. For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
  • Architecture and Design: For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page. One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
  • System Configuration, Installation: Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.

CWE-345: Insufficient Verification of Data Authenticity

The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.

CWE-352: Cross-Site Request Forgery (CSRF)

The web application does not, or cannot, sufficiently verify whether a request was intentionally provided by the user who sent the request, which could have originated from an unauthorized actor.

How to prevent it
  • Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482]. For example, use anti-CSRF packages such as the OWASP CSRFGuard. [REF-330] Another example is the ESAPI Session Management control, which includes a component for CSRF. [REF-45]
  • Implementation: Ensure that the application is free of cross-site scripting issues (CWE-79), because most CSRF defenses can be bypassed using attacker-controlled script.
  • Architecture and Design: Generate a unique nonce for each form, place the nonce into the form, and verify the nonce upon receipt of the form. Be sure that the nonce is not predictable (CWE-330). [REF-332]
  • Architecture and Design: Identify especially dangerous operations. When the user performs a dangerous operation, send a separate confirmation request to ensure that the user intended to perform that operation.
  • Architecture and Design: Use the "double-submitted cookie" method as described by Felten and Zeller: When a user visits a site, the site should generate a pseudorandom value and set it as a cookie on the user's machine. The site should require every form submission to include this value as a form value and also as a cookie value. When a POST request is sent to the site, the request should only be considered valid if the form value and the cookie value are the same. Because of the same-origin policy, an attacker cannot read or modify the value stored in the cookie. To successfully submit a form on behalf of the user, the attacker would have to correctly guess the pseudorandom value. If the pseudorandom value is cryptographically strong, this will be prohibitively difficult. This technique requires Javascript, so it may not work for browsers that have Javascript disabled. [REF-331]
  • Architecture and Design: Do not use the GET method for any request that triggers a state change.
  • Implementation: Check the HTTP Referer header to see if the request originated from an expected page. This could break legitimate functionality, because users or proxies may have disabled sending the Referer for privacy reasons.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-6x9p-4r67-5gjx
Budibase authenticated arbitrary S3 signed upload URL issuance via `/api/attachments/:datasourceId/url`
High Also affects @budibase/server
GHSA-5fpj-28rv-84r7
Budibase: SSRF in Automation Steps - Webhook, Zapier, N8N, Slack, Discord Bypass IP Blacklist
High Also affects @budibase/server
GHSA-hfhx-w8p8-4hc7
Budibase: SSRF via bare fetch() in uploadUrl during AI table generation
Medium Also affects @budibase/server
GHSA-v42f-v8xc-j435
Budibase: SSRF via DNS rebinding in the REST datasource integration
High Also affects @budibase/server
GHSA-pmpg-2mxq-6xwr
Budibase: NoSQL injection in MongoDB integration: collection dump, $where JS exec, cross-collection pivot, arbitrary update/delete
High Also affects @budibase/server
GHSA-cr7p-cr3q-h5cm
Budibase: Account Enumeration via Login Lockout Response Differential
Medium Also affects @budibase/server
GHSA-2xgg-r2wc-c5r2
Budibase: MySQL DESCRIBE Backtick Injection via multipleStatements in Database Connector
High Also affects @budibase/server
GHSA-qw6m-8fw2-2v64
Budibase: NoSQL Injection via JSON Parameter Interpolation in MongoDB Query Execution
High Also affects @budibase/server
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor