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.
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.
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:
// 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.
ID: gov-cmmc-level-1.identification-auth.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:
// 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
}