Summary
The Google OAuth login handlers in apps/webapp/app/models/user.server.ts silently link an attacker's
OAuth credentials to an existing victim account when they share the same email address — with no ownership
verification. An attacker who has a Google account whose email matches a victim's trigger.dev account
(created via magic link or the other OAuth provider) is immediately granted an authenticated session as the victim
upon OAuth login, resulting in full account takeover.
Details
Affected file: apps/webapp/app/models/user.server.ts
Vulnerable functions:
findOrCreateGoogleUser() — called by addGoogleStrategy in googleAuth.server.ts
Vulnerable code (Google):
const existingUser = await prisma.user.findUnique({
where: { authIdentifier },
});
const existingEmailUser = await prisma.user.findUnique({
where: { email }, // "victim@company.com" → victim's account found
});
if (existingEmailUser && !existingUser) {
const user = await prisma.user.update({
where: { email },
data: {
authIdentifier
authenticationProfile: authProfile,
authenticationExtraParams: authExtraParams,
avatarUrl,
},
});
return { user, isNewUser: false }; // victim's User object returned → attacker gets their session
}
Root cause: When an OAuth login arrives for a Google identity that has never been seen before (!existingUser)
but whose email matches an existing account (existingEmailUser), the server unconditionally writes the attacker's
OAuth identifier into the victim's database record and returns the victim's user object. No confirmation email, no
re-authentication of the original account holder, and no check that the accounts belong to the same person is
performed.
PoC
Prerequisites:
- Victim has a trigger.dev account at victim@company.com created via magic link (no OAuth linked)
- Attacker controls a Google account with victim@company.com as a verified (or even primary unverified) email
Steps:
1. Attacker navigates to the trigger.dev instance and starts Google OAuth login
2. Completes the Google flow using the victim@company.com Google account
3. Server processes the callback in findOrCreateGoogleUser:
- authIdentifier = "google:ATTACKER_ID" → no match (existingUser = null)
- email = "victim@company.com" → victim's account found (existingEmailUser set)
- Enters if (existingEmailUser && !existingUser) branch
- prisma.user.update(...) writes authIdentifier = "google:ATTACKER_ID" to victim's record
- Returns victim's user object
4. Remix Auth creates a session with userId = victim.id
5. Attacker is redirected to the dashboard, fully logged in as the victim
The same attack works with Google OAuth.
Impact
This is an Account Takeover vulnerability. Any trigger.dev user whose email address is also registered (as verified,
or potentially as unverified primary) on the attacker's Google account is at risk.
An attacker who exploits this gains:
- Full access to the victim's projects, tasks, runs, API keys, and environment variables
- Ability to create or revoke secret environment variables and API keys under the victim's identity
- Access to all organizations the victim belongs to
- Persistent access — the victim's original magic-link login method continues to work, making the compromise difficult
to detect
Summary
The Google OAuth login handlers in
apps/webapp/app/models/user.server.tssilently link an attacker'sOAuth credentials to an existing victim account when they share the same email address — with no ownership
verification. An attacker who has a Google account whose email matches a victim's trigger.dev account
(created via magic link or the other OAuth provider) is immediately granted an authenticated session as the victim
upon OAuth login, resulting in full account takeover.
Details
Affected file:
apps/webapp/app/models/user.server.tsVulnerable functions:
findOrCreateGoogleUser()— called byaddGoogleStrategyingoogleAuth.server.tsVulnerable code (Google):