GuardVibe
News
· 5 min read

Account Takeover by Email Match: Vendure, Sync-in, SSRF Guard Crash

Vendure linked external logins to existing accounts by unverified email, Sync-in's token endpoint skipped 2FA, request-filtering-agent crashes on private IPs.

Two authentication advisories this week share one flaw: a second way into an account that skips the check the main login enforces. Vendure stores with social or custom OAuth login could link an attacker's external identity to an existing customer by email alone, and Sync-in Server handed out tokens without asking for the second factor. A third advisory turns an SSRF guard into a one-request crash.

What shipped

@vendure/core: account takeover through external login

  • Affected: @vendure/core < 3.7.0. Fixed: 3.7.0.
  • Severity: critical, CVSS 9.1 — GHSA-6j36-r6pr-59x4 (CVE-2026-63472).

ExternalAuthenticationService.createCustomerAndUser() looked up an existing user by email address and attached the new external login method to it, whether or not the strategy said the email was verified. An attacker who can present a victim's email through a configured provider that does not prove ownership gets logged in as that customer: orders, addresses and personal data. Stores using only Vendure's built-in email/password login are not affected.

npm install @vendure/core@^3.7.0

After upgrading, check every custom AuthenticationStrategy: it must set verified: true only when the provider actually verified the email (for OIDC, the email_verified claim).

@sync-in/server: 2FA skipped on the token endpoint

  • Affected: @sync-in/server <= 2.3.0. Fixed: 2.4.0.
  • Severity: high, CVSS 8.1 — GHSA-92cr-jxw4-5wjg (CVE-2026-58269).

POST /api/auth/login enforced TOTP for accounts with 2FA on. POST /api/auth/token checked only the username and password, then returned access and refresh JWTs. Anyone holding a leaked password for a 2FA-protected account could bypass the second factor with one request.

npm install @sync-in/server@^2.4.0

request-filtering-agent: SSRF guard crashes the process

  • Affected: request-filtering-agent < 3.2.1. Fixed: 3.2.1.
  • Severity: high, CVSS 7.5 — GHSA-r3r9-wp5j-pq5g (CVE-2026-62985).

The agent blocks requests to private addresses, but for a literal private IP such as 169.254.169.254 it throws synchronously inside createConnection(). That throw escapes req.on('error') and kills the Node.js process. The library exists to fetch user-supplied URLs — webhooks, link previews, image proxies — so any user who can submit a URL can take the service down. Hostnames that only resolve to private IPs take a different code path and fail safely.

npm install request-filtering-agent@^3.2.1

Account linking by email, explained

Account linking is the step where a login from a new provider ("Sign in with GitHub") gets attached to an account that already exists. The common shortcut is to match on email: the provider returns alice@example.com, you find the user with that email, and you log them in. That shortcut is only safe if the provider has proven that the person logging in controls that inbox. Many do not guarantee it: custom OAuth servers, some enterprise IdPs, and any provider that lets users type an unconfirmed email into their profile. When the email is not verified, "same email" means "anyone who typed that email".

This keeps showing up in AI-generated code because the happy path looks complete. Ask an assistant for "add Google login and merge with existing accounts" and the natural completion is findUserByEmail(profile.email). It works in every manual test, since the developer's own email is verified at every provider they try. Nothing fails until someone presents an email they do not own.

A typical vulnerable callback:

// OAuth callback — links by email alone
const profile = await provider.getProfile(accessToken);
let user = await db.user.findUnique({ where: { email: profile.email } });
if (!user) user = await db.user.create({ data: { email: profile.email } });
await db.account.create({
  data: { userId: user.id, provider: "acme", providerAccountId: profile.sub },
});
return createSession(user.id);

The fixed version keys on the provider's stable subject id and only links to an existing account when the email is verified:

const profile = await provider.getProfile(accessToken);
const linked = await db.account.findUnique({
  where: { provider_providerAccountId: { provider: "acme", providerAccountId: profile.sub } },
});
if (linked) return createSession(linked.userId);

const existing = await db.user.findUnique({ where: { email: profile.email } });
if (existing) {
  if (profile.email_verified !== true) {
    throw new Error("Sign in with your existing method, then link this provider from settings.");
  }
  // optionally also require the user to be signed in already
}
const user = existing ?? await db.user.create({ data: { email: profile.email } });
await db.account.create({ data: { userId: user.id, provider: "acme", providerAccountId: profile.sub } });
return createSession(user.id);

Auth libraries usually ship with this off by default — Auth.js, for example, refuses to link a new provider to an existing email unless you opt in with allowDangerousEmailAccountLinking. The flag name is the warning.

The Sync-in bug is the same shape from another angle: two entry points to one account, and only one of them enforces the full policy. The review check covers both:

  • List every route that ends in a session or a token: login, token, refresh, magic link, OAuth callback, password reset, invite acceptance.
  • For each one, ask whether it applies the same checks as the main login: 2FA, verified email, account lock, tenant membership.
  • Wherever an account is found by email, ask who proved that email.

Check your own repo

npm ls @vendure/core @sync-in/server request-filtering-agent   # are any of these in your tree?
grep -rn "email_verified\|allowDangerousEmailAccountLinking" src/   # how does your OAuth code link accounts?
npm audit --omit=dev
npx guardvibe@3.39.0 audit .   # 3.39.0 flags vulnerable @vendure/core versions (VG1131); request-filtering-agent is covered by VG1127

Sources

Get the next one in your feed reader

Follow GuardVibe in your feed reader. No account, no email.