form-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseForm Security
表单安全
Security-first patterns for web forms. Ensures password manager compatibility, prevents common attacks, and protects user data.
优先保障安全的Web表单模式。确保与密码管理器兼容,防范常见攻击,保护用户数据。
Quick Start
快速开始
tsx
// The 3 critical security patterns
<form>
{/* 1. Autocomplete for password managers */}
<input type="email" autoComplete="email" />
<input type="password" autoComplete="current-password" />
{/* 2. CSRF token */}
<input type="hidden" name="_csrf" value={csrfToken} />
{/* 3. Allow paste (never disable!) */}
<input type="password" /> {/* No onPaste handler blocking */}
</form>tsx
// The 3 critical security patterns
<form>
{/* 1. Autocomplete for password managers */}
<input type="email" autoComplete="email" />
<input type="password" autoComplete="current-password" />
{/* 2. CSRF token */}
<input type="hidden" name="_csrf" value={csrfToken} />
{/* 3. Allow paste (never disable!) */}
<input type="password" /> {/* No onPaste handler blocking */}
</form>Autocomplete Attributes
Autocomplete属性
Why It Matters
重要性
- 1Password, LastPass, Bitwarden rely on to identify fields
autocomplete - Without correct values, password managers fail silently
- Users abandon forms when autofill doesn't work
- Security improves when users can use unique, strong passwords
- 1Password、LastPass、Bitwarden 依赖识别字段
autocomplete - 没有正确的属性值,密码管理器会静默失效
- 自动填充功能失效时,用户会放弃填写表单
- 当用户能使用唯一、强密码时,安全性会提升
The Autocomplete Specification
Autocomplete规范
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];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
<form action="/login">
<input type="email" autoComplete="email" />
<input type="password" autoComplete="current-password" />
</form>
// ✅ REGISTRATION: Use new-password (BOTH fields)
<form action="/register">
<input type="email" autoComplete="email" />
<input type="password" autoComplete="new-password" />
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ PASSWORD RESET: Use new-password
<form action="/reset-password">
<input type="password" autoComplete="new-password" />
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ CHANGE PASSWORD: current + new
<form action="/change-password">
<input type="password" autoComplete="current-password" /> {/* old */}
<input type="password" autoComplete="new-password" /> {/* new */}
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ 2FA/OTP: Use one-time-code
<form action="/verify-2fa">
<input
type="text"
inputMode="numeric"
autoComplete="one-time-code"
pattern="[0-9]*"
/>
</form>tsx
// ✅ LOGIN: Use current-password
<form action="/login">
<input type="email" autoComplete="email" />
<input type="password" autoComplete="current-password" />
</form>
// ✅ REGISTRATION: Use new-password (BOTH fields)
<form action="/register">
<input type="email" autoComplete="email" />
<input type="password" autoComplete="new-password" />
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ PASSWORD RESET: Use new-password
<form action="/reset-password">
<input type="password" autoComplete="new-password" />
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ CHANGE PASSWORD: current + new
<form action="/change-password">
<input type="password" autoComplete="current-password" /> {/* old */}
<input type="password" autoComplete="new-password" /> {/* new */}
<input type="password" autoComplete="new-password" /> {/* confirm */}
</form>
// ✅ 2FA/OTP: Use one-time-code
<form action="/verify-2fa">
<input
type="text"
inputMode="numeric"
autoComplete="one-time-code"
pattern="[0-9]*"
/>
</form>Why new-password
for Registration
new-password为什么注册表单要用new-password
new-passwordtsx
// ❌ WRONG: Using current-password on registration
// Password manager tries to fill EXISTING password
<input type="password" autoComplete="current-password" />
// ✅ CORRECT: Using new-password
// Password manager offers to GENERATE a new password
<input type="password" autoComplete="new-password" />tsx
// ❌ WRONG: Using current-password on registration
// Password manager tries to fill EXISTING password
<input type="password" autoComplete="current-password" />
// ✅ CORRECT: Using new-password
// Password manager offers to GENERATE a new password
<input type="password" autoComplete="new-password" />Payment Form Pattern
支付表单模式
tsx
<form action="/checkout">
<input
type="text"
autoComplete="cc-name"
placeholder="Name on card"
/>
<input
type="text"
inputMode="numeric"
autoComplete="cc-number"
placeholder="Card number"
/>
<input
type="text"
autoComplete="cc-exp"
placeholder="MM/YY"
/>
<input
type="text"
inputMode="numeric"
autoComplete="cc-csc"
placeholder="CVV"
/>
</form>tsx
<form action="/checkout">
<input
type="text"
autoComplete="cc-name"
placeholder="Name on card"
/>
<input
type="text"
inputMode="numeric"
autoComplete="cc-number"
placeholder="Card number"
/>
<input
type="text"
autoComplete="cc-exp"
placeholder="MM/YY"
/>
<input
type="text"
inputMode="numeric"
autoComplete="cc-csc"
placeholder="CVV"
/>
</form>Address Form Pattern
地址表单模式
tsx
<fieldset>
<legend>Shipping Address</legend>
<input autoComplete="name" placeholder="Full name" />
<input autoComplete="address-line1" placeholder="Street address" />
<input autoComplete="address-line2" placeholder="Apt, Suite, etc." />
<input autoComplete="address-level2" placeholder="City" />
<input autoComplete="address-level1" placeholder="State" />
<input autoComplete="postal-code" placeholder="ZIP code" />
<select autoComplete="country">
<option value="US">United States</option>
{/* ... */}
</select>
</fieldset>tsx
<fieldset>
<legend>Shipping Address</legend>
<input autoComplete="name" placeholder="Full name" />
<input autoComplete="address-line1" placeholder="Street address" />
<input autoComplete="address-line2" placeholder="Apt, Suite, etc." />
<input autoComplete="address-level2" placeholder="City" />
<input autoComplete="address-level1" placeholder="State" />
<input autoComplete="postal-code" placeholder="ZIP code" />
<select autoComplete="country">
<option value="US">United States</option>
{/* ... */}
</select>
</fieldset>CSRF Protection
CSRF防护
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();
});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 (
<form method="POST">
<input type="hidden" name="_csrf" value={csrfToken} />
{/* form fields */}
</form>
);
}
// 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)
});
}tsx
// React pattern
function Form({ csrfToken }) {
return (
<form method="POST">
<input type="hidden" name="_csrf" value={csrfToken} />
{/* form fields */}
</form>
);
}
// 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);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
双重提交Cookie模式
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 === headertypescript
// 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 === headerXSS Prevention
XSS预防
Never Trust User Input
绝不信任用户输入
typescript
// ❌ DANGEROUS: Directly rendering user input
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ SAFE: React auto-escapes by default
<div>{userInput}</div>
// ✅ SAFE: Explicit sanitization when HTML needed
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />typescript
// ❌ DANGEROUS: Directly rendering user input
<div dangerouslySetInnerHTML={{ __html: userInput }} />
// ✅ SAFE: React auto-escapes by default
<div>{userInput}</div>
// ✅ SAFE: Explicit sanitization when HTML needed
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />Input Sanitization
输入清理
typescript
// sanitize.ts
import DOMPurify from 'dompurify';
// For plain text (strip all HTML)
export function sanitizeText(input: string): string {
return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
}
// For rich text (allow safe HTML)
export function sanitizeHtml(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title']
});
}
// For URLs
export function sanitizeUrl(url: string): string {
const sanitized = DOMPurify.sanitize(url);
// Only allow http(s) and relative URLs
if (/^(https?:\/\/|\/[^\/])/i.test(sanitized)) {
return sanitized;
}
return '';
}typescript
// sanitize.ts
import DOMPurify from 'dompurify';
// For plain text (strip all HTML)
export function sanitizeText(input: string): string {
return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
}
// For rich text (allow safe HTML)
export function sanitizeHtml(input: string): string {
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title']
});
}
// For URLs
export function sanitizeUrl(url: string): string {
const sanitized = DOMPurify.sanitize(url);
// Only allow http(s) and relative URLs
if (/^(https?:\/\/|\/[^\/])/i.test(sanitized)) {
return sanitized;
}
return '';
}Content Security Policy
内容安全策略(CSP)
typescript
// Set CSP headers (Express)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
next();
});typescript
// Set CSP headers (Express)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'"
);
next();
});Password Field Security
密码字段安全
Never Disable Paste
绝不禁用粘贴
tsx
// ❌ DANGEROUS: Disabling paste
<input type="password" onPaste={(e) => e.preventDefault()} />
// ✅ CORRECT: Allow paste (password managers need it!)
<input type="password" />tsx
// ❌ DANGEROUS: Disabling paste
<input type="password" onPaste={(e) => e.preventDefault()} />
// ✅ CORRECT: Allow paste (password managers need it!)
<input type="password" />Password Visibility Toggle
密码可见性切换
tsx
function PasswordInput({ ...props }) {
const [visible, setVisible] = useState(false);
return (
<div className="password-input">
<input
type={visible ? 'text' : 'password'}
{...props}
/>
<button
type="button"
onClick={() => setVisible(!visible)}
aria-label={visible ? 'Hide password' : 'Show password'}
>
{visible ? <EyeOffIcon /> : <EyeIcon />}
</button>
</div>
);
}tsx
function PasswordInput({ ...props }) {
const [visible, setVisible] = useState(false);
return (
<div className="password-input">
<input
type={visible ? 'text' : 'password'}
{...props}
/>
<button
type="button"
onClick={() => setVisible(!visible)}
aria-label={visible ? 'Hide password' : 'Show password'}
>
{visible ? <EyeOffIcon /> : <EyeIcon />}
</button>
</div>
);
}Don't Log Passwords
不要记录密码
typescript
// ❌ DANGEROUS
console.log('Login attempt:', { email, password });
// ✅ SAFE
console.log('Login attempt:', { email, password: '[REDACTED]' });typescript
// ❌ DANGEROUS
console.log('Login attempt:', { email, password });
// ✅ SAFE
console.log('Login attempt:', { email, password: '[REDACTED]' });Secure Form Submission
安全表单提交
HTTPS Only
仅使用HTTPS
typescript
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});typescript
// Redirect HTTP to HTTPS
app.use((req, res, next) => {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
next();
});Secure Cookies
安全Cookie设置
typescript
// Set secure cookie flags
res.cookie('session', sessionId, {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});typescript
// Set secure cookie flags
res.cookie('session', sessionId, {
httpOnly: true, // Prevent XSS access
secure: true, // HTTPS only
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});Rate Limiting
速率限制
typescript
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts. Please try again later.'
});
app.post('/login', loginLimiter, handleLogin);typescript
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts. Please try again later.'
});
app.post('/login', loginLimiter, handleLogin);Security Checklist
安全检查清单
Authentication Forms
认证表单
- on email field
autocomplete="email" - on login password
autocomplete="current-password" - on registration/reset password
autocomplete="new-password" - on 2FA field
autocomplete="one-time-code" - Paste is allowed on all password fields
- CSRF token included
- Rate limiting enabled
- HTTPS enforced
- 邮箱字段使用
autocomplete="email" - 登录密码字段使用
autocomplete="current-password" - 注册/重置密码字段使用
autocomplete="new-password" - 2FA字段使用
autocomplete="one-time-code" - 所有密码字段允许粘贴
- 包含CSRF令牌
- 启用速率限制
- 强制使用HTTPS
Payment Forms
支付表单
- attributes on all card fields
autocomplete="cc-*" - on number fields
inputMode="numeric" - CSRF token included
- PCI DSS compliance (use Stripe/Braintree)
- No card data logged
- 所有卡片字段使用属性
autocomplete="cc-*" - 数字字段使用
inputMode="numeric" - 包含CSRF令牌
- 符合PCI DSS合规要求(使用Stripe/Braintree)
- 不记录卡片数据
All Forms
所有表单
- Input validation (client + server)
- Output encoding (XSS prevention)
- Error messages don't leak sensitive info
- Secure cookie settings
- CSP headers configured
- 输入验证(客户端+服务端)
- 输出编码(XSS预防)
- 错误信息不泄露敏感信息
- 安全Cookie设置
- 配置CSP头
File Structure
文件结构
form-security/
├── SKILL.md
├── references/
│ ├── autocomplete-spec.md # Full autocomplete reference
│ └── csrf-patterns.md # CSRF implementation patterns
└── scripts/
├── autocomplete-config.ts # Autocomplete constants
├── csrf-token.ts # CSRF token utilities
├── sanitize.ts # Input sanitization
└── secure-input.tsx # Secure input componentsform-security/
├── SKILL.md
├── references/
│ ├── autocomplete-spec.md # Full autocomplete reference
│ └── csrf-patterns.md # CSRF implementation patterns
└── scripts/
├── autocomplete-config.ts # Autocomplete constants
├── csrf-token.ts # CSRF token utilities
├── sanitize.ts # Input sanitization
└── secure-input.tsx # Secure input componentsReference
参考资料
- — Complete autocomplete attribute reference
references/autocomplete-spec.md - — CSRF implementation patterns
references/csrf-patterns.md
- — 完整的autocomplete属性参考
references/autocomplete-spec.md - — CSRF实现模式
references/csrf-patterns.md