form-security について
Web フォームを開発する人向けのセキュリティスキルです。パスワードマネージャーに対応する自動入力属性、CSRF・XSS 対策、パスワードや Cookie、ログイン試行の保護を扱います。
主な機能
- 01自動入力属性のガイダンス
- 02CSRF トークンのパターン
- 03XSS 対策とサニタイズ
- 04パスワードフィールドの保護
- 05セキュア Cookie とレート制限
活用シーン
- 01ログイン・登録フォームの構築
- 02決済フォーム送信の保護
- 03フォーム処理の XSS リスク確認
- 04パスワード処理への保護策の適用
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bbeierle12/ skill-mcp-claude --skill form-securityインストール後、Claude Code で/form-securityを入力して読み込まれたか確認します。
同梱ファイル_meta.jsonscripts/autocomplete-config.ts
詳細
フォームのセキュリティ
Web フォームのセキュリティを最優先するパターンです。パスワードマネージャーとの互換性を確保し、一般的な攻撃を防ぎ、ユーザーデータを保護します。
クイックスタート
// 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 属性
重要な理由
- 1Password、LastPass、Bitwarden はフィールドを識別するために
autocompleteに依存しています - 正しい値が設定されていないと、パスワードマネージャーはエラーを表示せずに機能しなくなります
- 自動入力が機能しないと、ユーザーはフォームの入力を中断します
- ユーザーが一意で強力なパスワードを使えると、セキュリティが向上します
autocomplete の仕様
// 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];
特に重要なパスワードのパターン
// ✅ 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>
登録に new-password を使う理由
// ❌ 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" />
決済フォームのパターン
<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>
住所フォームのパターン
<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 対策
トークンの生成(サーバー)
// 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();
});
トークンの追加(クライアント)
// 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)
});
}
トークンの検証(サーバー)
// 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);
ダブルサブミットCookieパターン
// 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 対策
ユーザー入力を決して信頼しない
// ❌ 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) }} />
入力のサニタイズ
// 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 '';
}
コンテンツセキュリティポリシー
// 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();
});
パスワードフィールドのセキュリティ
貼り付けを無効にしない
// ❌ DANGEROUS: Disabling paste
<input type="password" onPaste={(e) => e.preventDefault()} />
// ✅ CORRECT: Allow paste (password managers need it!)
<input type="password" />
パスワードの表示切り替え
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>
);
}
パスワードをログに記録しない
// ❌ DANGEROUS
console.log('Login attempt:', { email, password });
// ✅ SAFE
console.log('Login attempt:', { email, password: '[REDACTED]' });
フォーム送信のセキュリティ
HTTPS のみ使用
// 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();
});
セキュアなCookie
// 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
});
レート制限
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);
セキュリティチェックリスト
認証フォーム
- メールアドレスフィールドに
autocomplete="email"を設定 - ログインパスワードに
autocomplete="current-password"を設定 - 登録・パスワードリセット用のパスワードに
autocomplete="new-password"を設定 - 2FA フィールドに
autocomplete="one-time-code"を設定 - すべてのパスワードフィールドで貼り付けを許可
- CSRF トークンを含める
- レート制限を有効にする
- HTTPS を強制する
決済フォーム
- すべてのカードフィールドに
autocomplete="cc-*"属性を設定 - 数値フィールドに
inputMode="numeric"を設定 - CSRF トークンを含める
- PCI DSS に準拠する(Stripe/Braintree を使用)
- カードデータをログに記録しない
すべてのフォーム
- 入力を検証する(クライアント側とサーバー側)
- 出力をエンコードする(XSS 対策)
- エラーメッセージで機密情報を漏らさない
- セキュアなCookie設定を使用する
- CSP ヘッダーを設定する
ファイル構成
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 components
参考資料
references/autocomplete-spec.md— autocomplete 属性の完全なリファレンスreferences/csrf-patterns.md— CSRF の実装パターン
コメント
Markdown 対応