# Password policy meets NIST 800-63B guidelines

- **Pattern:** `ab-001556` (`gov-cmmc-level-1.identification-auth.password-complexity`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001556
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001556)

## Why it matters

CMMC 2.0 IA.L1-3.5.2 (NIST 800-171r2 3.5.2) and NIST SP 800-63B both require that password policies resist guessing attacks. A client-side-only minimum length of 6 characters — enforced in a React component but not in the API route — is trivially bypassed with a direct POST request. Short passwords combined with no rate limiting on authentication endpoints allow automated credential stuffing or brute-force attacks to succeed against FCI-bearing accounts within minutes. CWE-521 (Weak Password Requirements) and OWASP A07 name this directly. NIST 800-63B recommends 8-character minimums as a floor; 12+ is the current defensible standard.

## Severity rationale

High because client-side-only password enforcement is trivially bypassed, allowing weak credentials to protect FCI-bearing accounts with no server-side resistance to automated attacks.

## Remediation

Validate password length server-side using Zod or equivalent. Add rate limiting on every auth endpoint — 5 attempts per minute per IP is a reasonable starting point for CMMC contexts:

```ts
// lib/validation/auth.ts
import { z } from 'zod'
export const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(12, 'Password must be at least 12 characters').max(128),
})

// app/api/auth/register/route.ts
const attempts = new Map<string, { count: number; resetAt: number }>()
function checkRateLimit(ip: string): boolean {
  const now = Date.now()
  const rec = attempts.get(ip)
  if (!rec || rec.resetAt < now) { attempts.set(ip, { count: 1, resetAt: now + 60_000 }); return true }
  if (rec.count >= 5) return false
  rec.count++; return true
}

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
  if (!checkRateLimit(ip)) {
    return Response.json({ error: 'Too many attempts. Try again later.' }, { status: 429 })
  }
  const result = registerSchema.safeParse(await req.json())
  if (!result.success) return Response.json({ error: 'Invalid input' }, { status: 400 })
  // Proceed with result.data
}
```

For external auth providers (Supabase Auth, Auth0, Clerk), verify minimum password length is set in the provider dashboard — application code alone does not control it.

## Detection

- **ID:** `password-complexity`
- **Severity:** `high`
- **CMMC Practice:** Derived from IA.L1-3.5.2
- **What to look for:** Look for password validation rules in registration and password reset flows. Check for minimum length enforcement (12+ characters per NIST 800-63B recommendations), server-side validation (not just frontend), and rate limiting on authentication endpoints. Note that NIST 800-63B actually discourages forced complexity rules (uppercase/lowercase/number/symbol requirements) in favor of length — so check for appropriate length enforcement rather than arbitrary complexity. Look for rate limiting (brute force protection) on `/api/auth/login`, `/api/auth/register`, and password reset endpoints.
- **Pass criteria:** Count all authentication endpoints and check each for rate limiting. Minimum password length of at least 12 characters enforced server-side. Rate limiting present on at least 90% of authentication endpoints (login, registration, password reset). Report: "X of Y auth endpoints have rate limiting; minimum password length is Z characters."
- **Fail criteria:** No password policy found. Minimum length less than 8 characters. Password policy only enforced client-side (JavaScript can be bypassed). No rate limiting on authentication endpoints.
- **Skip (N/A) when:** Project uses an external auth provider (Auth0, Clerk, Supabase Auth, Firebase Auth) that manages password policy centrally — in this case, verify the provider is configured to enforce NIST-compliant minimums rather than the application code doing it.
- **Detail on fail:** Specify which requirements are missing. Example: `"Password minimum is 6 characters enforced only in RegisterForm.tsx (client-side). POST /api/auth/register accepts any length and has no rate limiting."` Keep under 500 characters.
- **Remediation:** Validate password length server-side with schema validation and add rate limiting:

  ```ts
  // lib/validation/auth.ts
  import { z } from 'zod'

  export const passwordSchema = z
    .string()
    .min(12, 'Password must be at least 12 characters')
    .max(128, 'Password must be 128 characters or fewer')

  export const registerSchema = z.object({
    email: z.string().email(),
    password: passwordSchema
  })

  // app/api/auth/register/route.ts
  import { registerSchema } from '@/lib/validation/auth'

  // Simple in-memory rate limiter (use Redis for production scale)
  const attempts = new Map<string, { count: number; resetAt: number }>()

  function checkRateLimit(ip: string): boolean {
    const now = Date.now()
    const record = attempts.get(ip)
    if (!record || record.resetAt < now) {
      attempts.set(ip, { count: 1, resetAt: now + 60_000 })
      return true
    }
    if (record.count >= 5) return false
    record.count++
    return true
  }

  export async function POST(req: Request) {
    const ip = req.headers.get('x-forwarded-for') ?? 'unknown'
    if (!checkRateLimit(ip)) {
      return Response.json({ error: 'Too many attempts. Try again later.' }, { status: 429 })
    }

    const body = await req.json()
    const result = registerSchema.safeParse(body)
    if (!result.success) {
      return Response.json({ error: 'Invalid input' }, { status: 400 })
    }

    // Proceed with registration using result.data
  }
  ```

---

## External references

- cmmc IA.L1-3.5.2
- cwe CWE-521
- owasp A07
- nist SP-800-171 3.5.2
- nist SP-800-63B

Taxons: access-control, regulatory-conformance

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