--- name: form-security description: Security patterns for web forms including autocomplete attributes for password managers, CSRF protection, XSS prevention, and input sanitization. Use when implementing authentication forms, payment forms, or any form handling sensitive data. --- # Form Security Security-first patterns for web forms. Ensures password manager compatibility, prevents common attacks, and protects user data. ## Quick Start ```tsx // The 3 critical security patterns
``` ## Autocomplete Attributes ### Why It Matters - **1Password, LastPass, Bitwarden** rely on `autocomplete` to identify fields - Without correct values, password managers fail silently - Users abandon forms when autofill doesn't work - Security improves when users can use unique, strong passwords ### The Autocomplete Specification ```typescript // autocomplete-config.ts export const AUTOCOMPLETE = { // ===== IDENTITY ===== name: 'name', // Full name honorificPrefix: 'honorific-prefix', // Mr., Mrs., Dr. givenName: 'given-name', // First name additionalName: 'additional-name', // Middle name familyName: 'family-name', // Last name honorificSuffix: 'honorific-suffix', // Jr., III nickname: 'nickname', // ===== AUTHENTICATION (CRITICAL) ===== email: 'email', username: 'username', currentPassword: 'current-password', // LOGIN forms newPassword: 'new-password', // REGISTRATION + RESET forms oneTimeCode: 'one-time-code', // 2FA/OTP codes // ===== CONTACT ===== tel: 'tel', // Full phone telCountryCode: 'tel-country-code', telNational: 'tel-national', telAreaCode: 'tel-area-code', telLocal: 'tel-local', telExtension: 'tel-extension', // ===== ADDRESS ===== streetAddress: 'street-address', // Full street (may be multiline) addressLine1: 'address-line1', // Street line 1 addressLine2: 'address-line2', // Apt, Suite, etc. addressLine3: 'address-line3', addressLevel1: 'address-level1', // State/Province addressLevel2: 'address-level2', // City addressLevel3: 'address-level3', // District addressLevel4: 'address-level4', // Neighborhood postalCode: 'postal-code', country: 'country', countryName: 'country-name', // ===== PAYMENT (CRITICAL) ===== ccName: 'cc-name', // Name on card ccGivenName: 'cc-given-name', ccFamilyName: 'cc-family-name', ccNumber: 'cc-number', // Card number ccExp: 'cc-exp', // Expiry (MM/YY) ccExpMonth: 'cc-exp-month', // Expiry month ccExpYear: 'cc-exp-year', // Expiry year ccCsc: 'cc-csc', // CVV/CVC ccType: 'cc-type', // Visa, Mastercard, etc. // ===== ORGANIZATION ===== organization: 'organization', organizationTitle: 'organization-title', // Job title // ===== DATES ===== bday: 'bday', // Full birthday bdayDay: 'bday-day', bdayMonth: 'bday-month', bdayYear: 'bday-year', // ===== OTHER ===== sex: 'sex', // Gender url: 'url', // Website photo: 'photo', // Photo URL language: 'language', // ===== SPECIAL VALUES ===== off: 'off', // Disable autofill (use sparingly!) on: 'on' // Enable autofill (default) } as const; export type AutocompleteValue = typeof AUTOCOMPLETE[keyof typeof AUTOCOMPLETE]; ``` ### Critical Password Patterns ```tsx // ✅ LOGIN: Use current-password // ✅ REGISTRATION: Use new-password (BOTH fields) // ✅ PASSWORD RESET: Use new-password // ✅ CHANGE PASSWORD: current + new // ✅ 2FA/OTP: Use one-time-code ``` ### Why `new-password` for Registration ```tsx // ❌ WRONG: Using current-password on registration // Password manager tries to fill EXISTING password // ✅ CORRECT: Using new-password // Password manager offers to GENERATE a new password ``` ### Payment Form Pattern ```tsx ``` ### Address Form Pattern ```tsx ``` ## CSRF Protection ### Token Generation (Server) ```typescript // server/csrf.ts import crypto from 'crypto'; export function generateCsrfToken(): string { return crypto.randomBytes(32).toString('hex'); } // Store in session app.use((req, res, next) => { if (!req.session.csrfToken) { req.session.csrfToken = generateCsrfToken(); } res.locals.csrfToken = req.session.csrfToken; next(); }); ``` ### Token Inclusion (Client) ```tsx // React pattern function Form({ csrfToken }) { return ( ); } // With fetch async function submitForm(data: FormData) { const response = await fetch('/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken }, body: JSON.stringify(data) }); } ``` ### Token Validation (Server) ```typescript // Middleware function validateCsrf(req, res, next) { const tokenFromBody = req.body._csrf; const tokenFromHeader = req.headers['x-csrf-token']; const sessionToken = req.session.csrfToken; const providedToken = tokenFromBody || tokenFromHeader; if (!providedToken || providedToken !== sessionToken) { return res.status(403).json({ error: 'Invalid CSRF token' }); } next(); } // Apply to state-changing routes app.post('/api/*', validateCsrf); app.put('/api/*', validateCsrf); app.delete('/api/*', validateCsrf); ``` ### Double Submit Cookie Pattern ```typescript // Alternative: Cookie + Header must match // Server sets cookie res.cookie('csrf', token, { httpOnly: false, sameSite: 'strict' }); // Client reads cookie and sends in header const csrfToken = document.cookie .split('; ') .find(row => row.startsWith('csrf=')) ?.split('=')[1]; fetch('/api/submit', { headers: { 'X-CSRF-Token': csrfToken } }); // Server validates cookie === header ``` ## XSS Prevention ### Never Trust User Input ```typescript // ❌ DANGEROUS: Directly rendering user input // ✅ SAFE: React auto-escapes by default