Without email verification, any user can register with someone else's email address, potentially locking the legitimate owner out of your service, triggering unwanted emails to that person, or establishing a fraudulent account linked to a real identity. CWE-287 (Improper Authentication) applies when you grant full account access without confirming control of the supplied credential. OWASP A07 (Identification and Authentication Failures) covers unverified account creation among its failure modes, and identity-proofing frameworks in NIST SP 800-63A treat unverified claimed identifiers as a category-one enrollment weakness.
High because unverified accounts enable account enumeration, email harassment of third parties, and fraudulent account creation at scale with no authentication of the claimed identity.
Send a verification email at registration and gate access to sensitive features until the email is confirmed. Use a time-limited, single-use token for the verification link:
// src/app/api/auth/register/route.ts
const verificationToken = crypto.randomBytes(32).toString('hex')
await db.user.create({
data: {
email, passwordHash,
emailVerified: false,
verificationToken: crypto.createHash('sha256').update(verificationToken).digest('hex'),
verificationExpiry: new Date(Date.now() + 86_400_000) // 24h
}
})
await sendVerificationEmail(email, verificationToken)
// In middleware or layout — redirect unverified users:
if (!session.user.emailVerified) redirect('/verify-email')
NextAuth, Clerk, and Supabase Auth all support email verification natively — check configuration to confirm it's enabled rather than assuming the default is on.
ID: saas-authentication.password-credential.email-verification
Severity: high
What to look for: Examine the registration flow. Check whether a verification email is sent and whether email verification is required before the user can access the application. Look for: an emailVerified or verifiedAt field on the user record, a verification token generation step during registration, and middleware or route logic that gates access on email verification status. Check if unverified accounts have access to sensitive features. Count all instances found and enumerate each.
Pass criteria: A verification email is sent on signup. Unverified accounts either cannot log in, or are gated out of sensitive features (creating content, billing, inviting others) until verified. The verification token is time-limited. At least 1 implementation must be confirmed.
Fail criteria: No email verification step — accounts are immediately fully active with no validation that the email address is real or accessible by the registrant.
Skip (N/A) when: No email/password registration — OAuth-only auth (providers verify the email themselves). Or invite-only application where access is granted only via pre-approved invites. Signal: no registration endpoint, or registration requires admin pre-approval.
Detail on fail: "Registration at /api/auth/register creates a fully active account without sending a verification email or setting emailVerified = false".
Remediation: Without email verification, anyone can register with someone else's email address, potentially locking that person out of your service. It also enables disposable email abuse:
// On registration:
const verificationToken = crypto.randomBytes(32).toString('hex')
await db.user.create({
data: {
email,
passwordHash,
emailVerified: false,
verificationToken: hash(verificationToken),
verificationExpiry: new Date(Date.now() + 24 * 60 * 60 * 1000)
}
})
await sendVerificationEmail(email, verificationToken)
// Gate access in middleware or layout:
if (!session.user.emailVerified) {
redirect('/verify-email')
}
Most auth libraries (NextAuth, Clerk, Supabase Auth) support email verification natively — check the configuration to ensure it's enabled.