mcpField
F
form-security

form-security

github.com/bbeierle12セキュリティ964 回インストール1 日前に更新
リポジトリ

form-security について

Web フォームを開発する人向けのセキュリティスキルです。パスワードマネージャーに対応する自動入力属性、CSRF・XSS 対策、パスワードや Cookie、ログイン試行の保護を扱います。

主な機能

  1. 01自動入力属性のガイダンス
  2. 02CSRF トークンのパターン
  3. 03XSS 対策とサニタイズ
  4. 04パスワードフィールドの保護
  5. 05セキュア Cookie とレート制限

活用シーン

  1. 01ログイン・登録フォームの構築
  2. 02決済フォーム送信の保護
  3. 03フォーム処理の XSS リスク確認
  4. 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 の実装パターン

コメント