# Email verification required for new accounts

- **Pattern:** `ab-000706` (`community-moderation-safety.spam-prevention.email-verification`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000706
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000706)

## Why it matters

Platforms without email verification allow spam accounts to be created and operational in seconds. CWE-306 (Missing Authentication for Critical Function) applies: posting is a critical function that should require verified identity. Without a verification gate, a single attacker can automate account creation and bypass per-account rate limits by rotating through throwaway emails. Email verification also enforces a basic contact point for ban notifications, password resets, and compliance communications — platforms without it lose the ability to reach users for any purpose.

## Severity rationale

High because unverified accounts eliminate the primary friction against spam account creation, allowing attackers to scale abuse by simply creating new accounts when banned.

## Remediation

Gate all content-creation endpoints behind an email verification check. Add a `emailVerified` boolean to your users table and enforce it at the API layer:

```javascript
app.post('/api/posts', authenticate, async (req, res) => {
  const user = await db.users.findOneById(req.user.id);
  if (!user.emailVerified) {
    return res.status(403).json({ error: 'Please verify your email before posting.' });
  }
  // ... create post
});
```

On signup, generate a time-limited token (24-hour expiry), send it to the provided email, and set `emailVerified = true` only when the token is consumed. A verification email that is sent but never enforced as a gate does not satisfy this check.

## Detection

- **ID:** `email-verification`
- **Severity:** `high`
- **What to look for:** Check if new user accounts require email verification before posting. Look for email verification flows, token generation, and verification checks before content submission.
- **Pass criteria:** New accounts must verify their email before posting content. Unverified accounts cannot submit posts or comments — at least 100% of post-creation endpoints must enforce this gate. Enumerate all post-creation endpoints and confirm each checks the email verification flag before allowing submission.
- **Fail criteria:** Accounts can post immediately upon creation without email verification. A verification email sent but never enforced as a gate does not count as pass.
- **Skip (N/A) when:** Platform allows anonymous posting with no user accounts required.
- **Detail on fail:** `"New users can post immediately. No email verification required. Spam accounts are trivial to create."`
- **Remediation:** Require email verification before new accounts can post content:

  ```javascript
  // Create account with unverified email
  app.post('/api/auth/signup', async (req, res) => {
    const { email, password } = req.body;

    const user = await db.users.create({
      email,
      passwordHash: await hash(password),
      emailVerified: false
    });

    // Generate and send verification token
    const token = generateToken();
    await db.verificationTokens.create({
      userId: user.id,
      token,
      expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000)
    });

    await sendEmail(
      email,
      `Verify your email: ${process.env.APP_URL}/verify?token=${token}`
    );

    res.status(201).json({ message: 'Check your email to verify your account' });
  });

  // Verify email endpoint
  app.post('/api/auth/verify', async (req, res) => {
    const { token } = req.body;
    const verification = await db.verificationTokens.findOne({ token });

    if (!verification || verification.expiresAt < new Date()) {
      return res.status(400).json({ error: 'Invalid or expired token' });
    }

    await db.users.updateOne(
      { _id: verification.userId },
      { emailVerified: true }
    );
    await db.verificationTokens.deleteOne({ _id: verification._id });

    res.status(200).json({ success: true });
  });

  // Check verification before posting
  app.post('/api/posts', authenticate, async (req, res) => {
    const user = await db.users.findOne({ _id: req.user.id });

    if (!user.emailVerified) {
      return res.status(403).json({ error: 'Please verify your email to post' });
    }

    // ... create post
  });
  ```

## External references

- cwe CWE-306

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-000706
