Summary
Fixes multiple critical vulnerabilities discovered during investigation of bulk account takeover attempts via Oxylabs residential proxy (79.116.161.224, Madrid, Spain). Datadog threat_intel flagged all requests as intention: suspicious, category: residential_proxy, risks: AD_FRAUD, CALLBACK_PROXY.
Attack vector identified
The OAuth callback endpoint echoed any Origin header with Access-Control-Allow-Credentials: true, and returned the JWT token in the response body. This enabled a bulk session-stealing attack:
- Attacker embeds JS on any page (ad network, compromised site)
- Any logged-in Composio user who visits that page — attacker's JS makes a credentialed cross-origin request to the callback endpoint
- Browser sends
.composio.dev session cookie (cookie domain is .composio.dev = all subdomains)
- Response with JWT is readable by attacker's JS due to permissive CORS
- Attacker replays stolen sessions from residential proxies
Changes
- CORS origin whitelist (
callback.ts): Replace echo-any-origin with isDashboardOrigin() validation. Only known dashboard origins get Access-Control-Allow-Origin + credentials: true. Unknown origins get no CORS headers.
- Redirect URI validation (
login.ts, callback.ts): Origin header validated against isDashboardOrigin() before constructing redirect URIs. Prevents open redirect to attacker-controlled domains.
- Redirect URI parameter validation (
login.ts): User-supplied redirectUri query param must start with the validated origin. Prevents arbitrary redirect injection.
- Account linking hardening (
better_auth/index.ts): Added trustedProviders: ['google', 'github'] to prevent untrusted providers from auto-linking to existing accounts by email.
- Cryptographic state parameter (
login.ts): Replaced Math.random().toString(36) with crypto.randomUUID().
Remaining follow-ups (not in this PR)
Test plan
🤖 Generated with Claude Code