Security research

The preview endpoint that handed over everyone's API keys

Dmitry Maranik

A published High-severity advisory in Typebot let any authenticated user — down to a read-only guest — decrypt and steal another workspace's stored secrets: LLM keys, SMTP passwords, OAuth tokens, payment keys. The mechanism is the same one behind most of the cross-tenant bugs we report: one code path checks that you belong to the workspace you're acting on, and a sibling path a few lines over simply trusts what the client sent.

Typebot is an open-source chatbot builder, multi-tenant by workspace: your bots, your integrations, and the API keys behind them are supposed to be walled off from every other workspace on the same instance. On 2026-08-21 the maintainer published GHSA-pqrh-5jh7-xvcg, a High-severity cross-workspace credential-theft bug (CWE-639 + CWE-863), fixed in Typebot 3.18.0. This is a walk through what it was, why it worked, and the one-line-shaped fix — plus an honest note on how we were involved.

What broke

Typebot's editor has a preview chat: you're building a bot, you hit play, and the server runs the bot definition you're editing so you can try it. The important detail is that in preview mode the client sends the entire bot object, and the server runs it. That object carries two things that turn out to matter a great deal: a workspaceId, and, on each integration block, a credentialsId.

The preview endpoint checks that you're logged in. It never checks that you belong to the workspace the bot claims. So the workspace the server acts as is whatever the client put in the JSON.

The chain, in four steps

The whole bug is that a single attacker-controlled field — workspaceId — flows from the request body into credential decryption without anyone re-checking it against the caller. Following it through Typebot's source:

  1. The preview endpoint returns the client's bot unchanged. POST /api/v1/typebots/{id}/preview/startChat takes an optional typebot object; in preview mode the server returns it as-is the moment any user id is present, without checking membership (startSession.ts). The live path, by contrast, loads the bot from the database, so its workspace is trusted there. That asymmetry is the whole bug.
  2. The client's workspaceId becomes the session's workspace. It's copied straight into the session state (initialState.workspaceId = typebot.workspaceId).
  3. Credential lookup authorizes on that workspace alone. getCredentials returns a credential as long as credentials.workspaceId === workspaceId — and never checks the user (getCredentials.ts).
  4. Each integration block resolves its credential with the session workspace. The forge runtime behind every modern integration — OpenAI, SMTP, payment — calls getCredentials(block.options.credentialsId, state.workspaceId) (executeForgedBlock.ts). Both arguments came straight from the attacker's submitted bot, so the only guard passes for any victim credential the attacker names.

Put together: an attacker submits a bot that claims a victim workspace and references one of that workspace's credentials. The server decrypts the secret and uses it. Point an integration block's baseUrl at a server the attacker controls, and the decrypted key is delivered straight to them.

Why it matters more than it looks

Two things make this worse than a typical IDOR. First, a guest can do it. The attack needs a target workspaceId and a credentialsId inside it, and neither is secret to a workspace member — both sit in clear text inside the workspace's bots, which even the lowest GUEST role can read. A role meant to grant read-only access becomes a way to retrieve and use every credential in the workspace. Second, what leaks is live secrets, not metadata: provider API keys, SMTP passwords, OAuth tokens, payment keys — decrypted server-side and handed to an endpoint the attacker chose.

The pattern: the live path checks, the sibling trusts

We've written before about the un-retrofitted read sibling — a tenant check that's wired into the write and list paths but skipped on a neighboring read. This is the same shape, one level up. The live chat path loads the bot from the database and inherits a trusted workspace. The preview path, sitting right beside it, trusts the workspace the client asserts. Same feature, two entry points, and only one of them enforces the boundary.

That's why these bugs are so common in multi-tenant AI: the isolation logic is usually correct where it was first written, and the leak lives in the second, third, or fourth place the same object is loaded — a preview endpoint, a background job, an LLM-tool call — where someone reused the query but not the check.

The fix

One check, in the one place the boundary was missing: before trusting a submitted preview bot, confirm the caller is a member of the workspace it claims, so the session's workspaceId can never be attacker-chosen.

Our part, honestly

We're credited as a co-reporter on this advisory, and it's worth being precise about what that means. Our report was a related cross-workspace read — a TypebotLink block that loaded another workspace's private bot content through the same client-controlled workspace binding (tracked as GHSA-p258-9pjj-3v8f). The maintainer consolidated it into this advisory because it shares the root cause. The credential-theft escalation — turning that binding into secret exfiltration — was reported independently by other researchers — colinthebomb1 and arpitjain099 are credited alongside us. We're writing it up because the underlying pattern is exactly the one our research is about, and because a clean public example of it is more useful than another abstract description.

Check your own preview and trust-the-client paths

If you build anything multi-tenant, the question this bug asks is: where does a workspace / tenant / org id come from, and is it re-checked against the caller every time it's used to reach data or secrets? The dangerous version is any endpoint that accepts a client-supplied object and acts as the workspace named inside it — previews, imports, webhooks, "try it" flows.

We built Sectum AI to answer that at runtime: it stands up synthetic workspaces, plants canary secrets, and checks whether one workspace's identity can reach another's data or credentials through your AI surfaces — with a signed evidence pack you can verify yourself. It's open source; point it at your own stack.

Explore the open source More disclosures

Advisory: GHSA-pqrh-5jh7-xvcg (published 2026-08-21, High, CWE-639/863), fixed in Typebot 3.18.0. Credited co-reporters: Dmitry Maranik (Sectum AI), colinthebomb1, and arpitjain099.