Security Vulnerability: Account Takeover via Username Collision in Registration Flow
Summary
A critical security vulnerability exists in the registration flow that allows an attacker to gain access to an existing user's account by registering with a different email address that shares the same username prefix.
Severity: Critical
Vulnerability Details
Root Cause Analysis
The vulnerability stems from two issues:
1. find_or_create_identity uses username as a lookup key
File: backend/app/services/registration_service.py:121-124
# Try to find by username
if not identity and username:
res = await db.execute(select(Identity).where(Identity.username == username))
identity = res.scalar_one_or_none()
This function searches for existing identities by email, phone, AND username. If a username match is found, it returns the existing identity without verifying ownership.
2. _handle_normal_register lacks password verification
File: backend/app/api/auth.py:344
identity = await registration_service.find_or_create_identity(
db,
email=data.email,
username=data.username,
password=data.password,
is_platform_admin=is_first_user
)
# NO PASSWORD VERIFICATION HERE!
Compare with the secure implementation in register_init (auth.py:181-186):
# If identity existed, verify password
if identity.password_hash and not verify_password(data.password, identity.password_hash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Email already registered. Incorrect password."
)
3. Frontend derives username from email
File: frontend/src/pages/Login.tsx:119
username: form.login_identifier.split('@')[0], // e.g., "john" from "john@gmail.com"
Attack Scenario
- Victim registers with
john@gmail.com, username john, password secret123
- Attacker registers with
john@yahoo.com, username john, password attacker456
What happens:
find_or_create_identity searches for john@yahoo.com email → not found
find_or_create_identity searches for john username → found victim's identity!
_handle_normal_register creates a new User record linked to victim's Identity
- Attacker successfully "registers" and gains access to victim's companies
Result:
- Attacker and victim share the same
Identity record
- Attacker can see all companies the victim belongs to
- Attacker can switch between victim's companies
- Both users can login with their respective passwords
Database State After Attack
identities table:
+----------------------------------+------------------+----------+
| id | email | username |
+----------------------------------+------------------+----------+
| uuid-victim | john@gmail.com | john |
+----------------------------------+------------------+----------+
users table:
+----------------------------------+----------------------------------+------------------+
| id | identity_id | tenant_id |
+----------------------------------+----------------------------------+------------------+
| uuid-user-1 | uuid-victim | company-A |
| uuid-user-2 (attacker's entry) | uuid-victim | company-B |
+----------------------------------+----------------------------------+------------------+
Impact
| Impact Type |
Description |
| Account Takeover |
Attacker gains access to victim's organizations |
| Data Breach |
Attacker can view victim's agents, conversations, and data |
| Privilege Escalation |
Attacker may inherit victim's roles across organizations |
| Misleading Errors |
If passwords differ, users see "Email already registered" for an email they never used |
Affected Code Paths
| Endpoint |
Function |
Has Password Check |
Vulnerable |
/auth/register |
_handle_normal_register |
❌ No |
✅ Yes |
/auth/register/init |
register_init |
✅ Yes |
❌ No |
| SSO flows |
Various |
N/A (OAuth) |
❌ No |
| Channel user service |
create_user_from_external |
N/A |
⚠️ Potential |
Proof of Concept
Scenario 1: Same Password (Critical - Account Takeover)
# Step 1: Victim registers
curl -X POST /api/auth/register \
-d '{"email":"victim@gmail.com","password":"shared123","display_name":"victim"}'
# Step 2: Attacker registers with different email, same username prefix
curl -X POST /api/auth/register \
-d '{"email":"victim@yahoo.com","password":"shared123","display_name":"attacker"}'
# Result: Both users share the same Identity!
# Attacker can now login and see victim's companies
Scenario 2: Different Password (Misleading Error)
# Step 1: Victim registers with password "secret123"
# Step 2: Attacker tries to register with password "hacker456"
# Result: "Email already registered. Incorrect password."
# (But attacker's email was never registered!)
Recommended Fix
Option 1: Add password verification (Quick fix)
In _handle_normal_register (auth.py), add after line 344:
identity = await registration_service.find_or_create_identity(...)
# ADD THIS:
if identity.email != data.email and identity.password_hash:
if not verify_password(data.password, identity.password_hash):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username already taken. Please choose a different username."
)
Option 2: Remove username lookup (Recommended)
Modify find_or_create_identity to only search by email/phone:
async def find_or_create_identity(...) -> Identity:
identity = None
# Only search by email and phone, NOT username
if email:
res = await db.execute(select(Identity).where(Identity.email == email))
identity = res.scalar_one_or_none()
if not identity and phone:
normalized_phone = re.sub(r"[\s\-\+]", "", phone)
res = await db.execute(select(Identity).where(Identity.phone == normalized_phone))
identity = res.scalar_one_or_none()
if identity:
return identity
# Handle username collision for new identities
final_username = username
if username:
existing = await db.execute(select(Identity).where(Identity.username == username))
if existing.scalar_one_or_none():
final_username = f"{username}_{uuid.uuid4().hex[:6]}"
# Create new identity with unique username
identity = Identity(
email=email,
phone=normalized_phone,
username=final_username,
...
)
Option 3: Frontend generates unique usernames
Instead of deriving username from email, generate a unique identifier:
username: `${form.login_identifier.split('@')[0]}_${Date.now().toString(36)}`,
Environment
- Clawith version: v1.8.1+
- Affected versions: All versions with the current registration flow
Credit
Discovered during testing of invitation code registration flow.
CVSS Score Estimate: 8.1 (High)
CWE: CWE-287 (Improper Authentication), CWE-288 (Authentication Bypass Using an Alternate Path)
Security Vulnerability: Account Takeover via Username Collision in Registration Flow
Summary
A critical security vulnerability exists in the registration flow that allows an attacker to gain access to an existing user's account by registering with a different email address that shares the same username prefix.
Severity: Critical
Vulnerability Details
Root Cause Analysis
The vulnerability stems from two issues:
1.
find_or_create_identityuses username as a lookup keyFile:
backend/app/services/registration_service.py:121-124This function searches for existing identities by email, phone, AND username. If a username match is found, it returns the existing identity without verifying ownership.
2.
_handle_normal_registerlacks password verificationFile:
backend/app/api/auth.py:344Compare with the secure implementation in
register_init(auth.py:181-186):3. Frontend derives username from email
File:
frontend/src/pages/Login.tsx:119Attack Scenario
john@gmail.com, usernamejohn, passwordsecret123john@yahoo.com, usernamejohn, passwordattacker456What happens:
find_or_create_identitysearches forjohn@yahoo.comemail → not foundfind_or_create_identitysearches forjohnusername → found victim's identity!_handle_normal_registercreates a new User record linked to victim's IdentityResult:
IdentityrecordDatabase State After Attack
Impact
Affected Code Paths
/auth/register_handle_normal_register/auth/register/initregister_initcreate_user_from_externalProof of Concept
Scenario 1: Same Password (Critical - Account Takeover)
Scenario 2: Different Password (Misleading Error)
Recommended Fix
Option 1: Add password verification (Quick fix)
In
_handle_normal_register(auth.py), add after line 344:Option 2: Remove username lookup (Recommended)
Modify
find_or_create_identityto only search by email/phone:Option 3: Frontend generates unique usernames
Instead of deriving username from email, generate a unique identifier:
Environment
Credit
Discovered during testing of invitation code registration flow.
CVSS Score Estimate: 8.1 (High)
CWE: CWE-287 (Improper Authentication), CWE-288 (Authentication Bypass Using an Alternate Path)