VibeSec-Skill について
Web アプリケーションを扱う AI アシスタント向けのセキュアコーディングガイドです。実装やコードレビューで参照できる、クライアント側とサーバー側の脆弱性対策をまとめています。
主な機能
- 01アクセス制御と認可チェック
- 02XSS・CSRF 対策
- 03秘密情報と機密データの保護
- 04オープンリダイレクト対策
- 05パスワード保存の指針
- 06SSRF の緩和策
活用シーン
- 01リソース認可ロジックの確認
- 02フォームやページの XSS リスク確認
- 03状態変更時の CSRF 対策確認
- 04リダイレクトと URL 取得処理の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ behisecc/ vibesec-skill --skill vibesec-skillインストール後、Claude Code で/vibesec-skillを入力して読み込まれたか確認します。
同梱ファイルREADME.mdLICENSE
詳細
Web アプリケーション向けセキュアコーディングガイド
概要
このガイドでは、Web アプリケーション向けの包括的なセキュアコーディングの実践方法を説明します。AI アシスタントとして、コードをバグハンターの視点から確認し、機能を損なうことなくアプリケーションを可能な限り安全にすることが役割です。
主な原則:
- 多層防御:単一のセキュリティ制御に決して依存しない
- 安全側に倒す:問題が発生した場合は、アクセスを拒否する
- 最小権限:必要最小限の権限を付与する
- 入力の検証:ユーザー入力を決して信用せず、すべてサーバー側で検証する
- 出力のエンコード:データが表示されるコンテキストに応じて適切にエンコードする
アクセス制御の問題
アクセス制御の脆弱性は、ユーザーが意図された権限を超えてリソースにアクセスしたり、操作を実行したりできる場合に発生します。
基本要件
認証が必要なすべてのデータ項目と操作について、次の要件を満たします。
-
ユーザーレベルの認可
- 各ユーザーがアクセスまたは変更できるのは、自分のデータだけでなければなりません
- ほかのユーザーや組織のデータにアクセスできないようにします
- 所有権はルートレベルだけでなく、必ずデータレイヤーで確認します
-
連番 ID ではなく UUID を使用する
- UUIDv4 など、推測できない識別子を使用します
- 例外:ユーザーから明示的に依頼された場合に限り、連番 ID を使用します
-
アカウントのライフサイクルへの対応
- ユーザーが組織から削除された場合:すべてのアクセストークンとセッションを直ちに無効化します
- アカウントが削除または無効化された場合:すべての有効なセッションと API キーを無効化します
- トークンの失効リストを実装するか、更新の仕組みを備えた短命トークンを使用します
認可チェックリスト
- すべてのリクエストで、ユーザーがリソースの所有者であることを確認する(クライアント側のデータを信用しない)
- マルチテナントアプリでは、組織への所属を確認する
- ロールベースの操作では、ロールの権限を検証する
- 権限の変更後は、権限を再検証する
- 親リソースの所有権を確認する(例:コメントにアクセスする場合、親投稿の所有者がユーザーであることを確認する)
避けるべきよくある落とし穴
- IDOR(安全でない直接オブジェクト参照):リクエストしたユーザーが、指定されたリソース ID にアクセスする権限を持つことを必ず確認します
- 権限昇格:ロールの変更はサーバー側で検証し、クライアントからのロール情報を決して信用しません
- 水平アクセス:同じ権限レベルのユーザー A が、ユーザー B のリソースにアクセスすること
- 垂直アクセス:一般ユーザーが管理者機能にアクセスすること
- 大量割り当て:ユーザーが更新できるフィールドを絞り込み、リクエストボディのすべてのフィールドを無条件に受け入れないようにします
実装パターン
# Pseudocode for secure resource access
function getResource(resourceId, currentUser):
resource = database.find(resourceId)
if resource is null:
return 404 # Don't reveal if resource exists
if resource.ownerId != currentUser.id:
if not currentUser.hasOrgAccess(resource.orgId):
return 404 # Return 404, not 403, to prevent enumeration
return resource
クライアント側のバグ
クロスサイトスクリプティング(XSS)
ユーザーが直接または間接的に制御できるすべての入力について、XSS を防ぐためにサニタイズする必要があります。
保護すべき入力元
直接入力:
- フォームフィールド(メールアドレス、名前、自己紹介、コメントなど)
- 検索クエリ
- アップロード時のファイル名
- リッチテキストエディター/WYSIWYG コンテンツ
間接入力:
- URL パラメーターとクエリ文字列
- URL フラグメント(ハッシュ値)
- アプリケーションで使用する HTTP ヘッダー(画面に表示する場合の Referer、User-Agent)
- ユーザーに表示するサードパーティ API のデータ
- WebSocket メッセージ
- iframe からの postMessage データ
- 表示する場合の LocalStorage/SessionStorage の値
見落とされがちな入力:
- ユーザー入力を反映するエラーメッセージ
- HTML を受け入れる PDF/ドキュメント生成ツール
- ユーザーデータを含むメールテンプレート
- 管理者パネルのログビューアー
- HTML として表示される JSON レスポンス
- JavaScript を含む可能性がある SVG ファイルのアップロード
- HTML を許可する場合の Markdown レンダリング
保護戦略
-
出力エンコーディング(コンテキスト固有)
- HTML コンテキスト:HTML エンティティにエンコード(
<→<) - JavaScript コンテキスト:JavaScript エスケープ
- URL コンテキスト:URL エンコード
- CSS コンテキスト:CSS エスケープ
- フレームワーク組み込みのエスケープ機能を使用します(React の JSX、Vue の {{ }} など)。
- HTML コンテキスト:HTML エンティティにエンコード(
-
コンテンツセキュリティポリシー(CSP)
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self';- スクリプトでは
'unsafe-inline'と'unsafe-eval'を避けます - 必要に応じて、インラインスクリプトに nonce またはハッシュを使用します
- 違反を報告します:
report-uri /csp-report
- スクリプトでは
-
入力のサニタイズ
- 実績のあるライブラリを使用します(HTML には DOMPurify)
- リッチテキストで許可するタグや属性をホワイトリストに登録します
- 危険なパターンを削除するか、エンコードします
-
追加ヘッダー
X-Content-Type-Options: nosniffX-Frame-Options: DENY(または CSP の frame-ancestors を使用)
クロスサイトリクエストフォージェリ(CSRF)
状態を変更するすべてのエンドポイントを、CSRF 攻撃から保護する必要があります。
CSRF 保護が必要なエンドポイント
認証済みの操作:
- すべての POST、PUT、PATCH、DELETE リクエスト
- 状態を変更する GET リクエスト(適切な HTTP メソッドを使用するよう修正します)
- ファイルのアップロード
- 設定の変更
- 決済/取引エンドポイント
認証前の操作:
- ログインエンドポイント(ログイン CSRF を防止します)
- サインアップエンドポイント
- パスワードリセット要求エンドポイント
- パスワード変更エンドポイント
- メール/電話番号の確認エンドポイント
- OAuth コールバックエンドポイント
保護メカニズム
-
CSRF トークン
- 暗号学的に安全な乱数トークンを生成します
- トークンをユーザーセッションに紐付けます
- 状態を変更するリクエストごとに検証します
- ログイン後に再生成します(セッション固定との組み合わせを防止します)
-
SameSite Cookie
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnlyStrict:Cookie はクロスサイトで送信されません(最も高いセキュリティ)Lax:Cookie はトップレベルのナビゲーションで送信されます(バランスが良好)- 多層防御のため、常に CSRF トークンと併用します
-
二重送信 Cookie パターン
- CSRF トークンを Cookie とリクエスト本文/ヘッダーの両方で送信します
- サーバーで両者が一致することを検証します
エッジケースとよくある間違い
- トークンの存在確認:CSRF 検証をトークンの有無に依存させてはいけません。常にトークンを必須にします
- フォームごとのトークン:機密性の高い操作では、フォームごとに一意のトークンを使用することを検討します
- JSON API:JSON の Content-Type だけで CSRF を防げると思い込まないでください。Origin/Referer ヘッダーを検証し、トークンも使用します
- CORS の設定ミス:許可範囲が広すぎる CORS 設定は SameSite Cookie を回避される原因になります
- サブドメイン:サブドメインの乗っ取りが CSRF につながる可能性があるため、CSRF トークンの適用範囲を限定します
- Flash/PDF のアップロード:レガシーブラウザーのプラグインによって SameSite が回避される可能性があります
- 副作用のある GET リクエスト:GET で状態を変更してはいけません
- トークン漏えい:CSRF トークンを URL に含めないでください
- URL 内のトークンとヘッダー内のトークン:URL パラメーターよりカスタムヘッダー(X-CSRF-Token)を優先します
検証チェックリスト
- トークンは暗号学的に安全な乱数です(安全な乱数生成器を使用)
- トークンはユーザーセッションに紐付いています
- 状態を変更するすべてのリクエストで、サーバー側でトークンが検証されます
- トークンがない場合、リクエストは拒否されます
- 認証状態が変化したときにトークンが再生成されます
- SameSite Cookie 属性が設定されています
- セッション Cookie に Secure フラグと HttpOnly フラグが設定されています
シークレットキーと機密データの漏えい
クライアント側のコードから、シークレットや機密情報にアクセスできないようにする必要があります。
クライアント側のコードで決して公開してはいけない情報
API キーとシークレット:
- サードパーティの API キー(Stripe、AWS など)
- データベース接続文字列
- JWT 署名シークレット
- 暗号化キー
- OAuth クライアントシークレット
- 内部サービスの URL/認証情報
機密性の高いユーザーデータ:
- クレジットカード番号全桁
- 社会保障番号
- パスワード(ハッシュ化済みのものも含む)
- セキュリティの質問と回答
- 電話番号全桁(マスクします:--1234)
- 表示に不要な機密性の高い PII
インフラの詳細情報:
- 内部 IP アドレス
- データベーススキーマ
- デバッグ情報
- 本番環境でのスタックトレース
- サーバーソフトウェアのバージョン
シークレットが隠れている場所(ここを確認してください!)
- JavaScript バンドル(ソースマップを含む)
- HTML コメント
- 非表示のフォームフィールド
- データ属性
- LocalStorage/SessionStorage
- SSR アプリの初期状態/ハイドレーションデータ
- ビルドツール経由で公開される環境変数(NEXT_PUBLIC_、REACT_APP_)
ベストプラクティス
- 環境変数:シークレットは
.envファイルに保存します - サーバーサイドのみ:シークレットが必要な API 呼び出しは、バックエンドからのみ行います
オープンリダイレクト
URL を受け取ってリダイレクトするエンドポイントは、すべてオープンリダイレクト攻撃から保護する必要があります。
保護戦略
-
許可リストによる検証
allowed_domains = ['yourdomain.com', 'app.yourdomain.com'] function isValidRedirect(url): parsed = parseUrl(url) return parsed.hostname in allowed_domains -
相対 URL のみ
- 完全な URL ではなく、パス(例:
/dashboard)のみを受け付けます - パスが
/で始まり、//を含まないことを検証します
- 完全な URL ではなく、パス(例:
-
間接参照
- 生の URL の代わりにマッピングを使用します:
?redirect=dashboard→/dashboardを検索
- 生の URL の代わりにマッピングを使用します:
防止すべきバイパス手法
| 手法 | 例 | 有効な理由 |
|---|---|---|
| @ 記号 | https://[email protected] | ブラウザーは legit.com をユーザー名として扱い、evil.com に移動します |
| サブドメインの悪用 | https://legit.com.evil.com | サブドメインは evil.com が所有しています |
| プロトコルの細工 | javascript:alert(1) | リダイレクト経由での XSS |
| URL の二重エンコード | %252f%252fevil.com | 二重デコード後に //evil.com になります |
| バックスラッシュ | https://legit.com\@evil.com | 一部のパーサーは \ を / に正規化します |
| ヌルバイト | https://legit.com%00.evil.com | 一部のパーサーはヌルバイトで切り詰めます |
| タブ/改行 | https://legit.com%09.evil.com | 空白文字の誤認を利用します |
| Unicode 正規化 | https://legіt.com(キリル文字の і) | IDN ホモグラフ攻撃 |
| Data URL | data:text/html,<script>... | ペイロードを直接実行します |
| プロトコル相対 URL | //evil.com | 現在のページのプロトコルを使用します |
| フラグメントの悪用 | https://legit.com#@evil.com | ライブラリによって解析方法が異なります |
IDN ホモグラフ攻撃への保護
- 検証前に URL を Punycode に変換します
- 機密性の高いリダイレクトでは、ASCII 以外のドメインを全面的にブロックすることを検討します
パスワードのセキュリティ
パスワードの要件
- 最小 8 文字(12 文字以上を推奨)
- 最大長を設けない(または 128 文字など、非常に長く設定する)
- 特殊文字を含むすべての文字を許可する
- 特定の文字種を必須にしない(ユーザーが強力なパスワードを選べるようにする)
保存
- Argon2id、bcrypt、または scrypt を使用します
- MD5、SHA1、プレーンな SHA256 は決して使用しないでください
サーバーサイドのバグ
サーバーサイドリクエストフォージェリ(SSRF)
サーバーがユーザーから提供または影響を受けた URL にリクエストを送信する機能は、すべて保護する必要があります。
脆弱性の可能性がある機能
- Webhook(ユーザーがコールバック URL を指定)
- URL プレビュー
- URL から PDF を生成する機能
- URL から画像/ファイルを取得する機能
- URL からインポートする機能
- RSS/フィードリーダー
- ユーザーが指定したエンドポイントとの API 連携
- プロキシ機能
- HTML から PDF/画像への変換機能
保護戦略
-
許可リスト方式(推奨)
- 事前に承認されたドメインへのリクエストのみを許可します
- 連携先には厳格な許可リストを維持します
-
ネットワークの分離
- URL 取得サービスを隔離されたネットワークで実行します
- 内部ネットワークやクラウドメタデータへのアクセスをブロックします
ブロックすべき IP および DNS バイパス手法
| 手法 | 例 | 説明 |
|---|---|---|
| 10 進数 IP | http://2130706433 | 127.0.0.1 を 10 進数で表記 |
| 8 進数 IP | http://0177.0.0.1 | 8 進数表記 |
| 16 進数 IP | http://0x7f.0x0.0x0.0x1 | 16 進数表記 |
| IPv6 の localhost | http://[::1] | IPv6 ループバック |
| IPv6 マップ IPv4 | http://[::ffff:127.0.0.1] | IPv4 マップ IPv6 |
| 短縮 IPv6 | http://[::] | すべてゼロ |
| DNS リバインディング | 攻撃者の DNS が内部 IP を返す | 1 回目のリクエストは外部 IP に解決され、2 回目は内部 IP に解決される |
| 内部への CNAME | 攻撃者ドメインが内部へ CNAME する | DNS が内部ホスト名を指す |
| URL パーサーの混乱 | http://attacker.com#@internal | パーサーごとに異なる動作 |
| リダイレクトチェーン | 外部 URL が内部へリダイレクトする | リダイレクトを慎重に追跡する |
| IPv6 スコープ ID | http://[fe80::1%25eth0] | インターフェーススコープの IPv6 |
| 珍しい IP 形式 | http://127.1 | 短縮された IP 表記 |
DNS リバインディングの防止
- リクエストを行う前に DNS を解決する
- 解決された IP が内部でないことを検証する
- リクエストに対して解決済み IP を固定する(再解決しない)
- または: 遅延を挟んで 2 回解決し、どちらも同じ外部 IP に解決されることを確認する
クラウドメタデータの保護
クラウドメタデータのエンドポイントへのアクセスをブロックします:
- AWS:
169.254.169.254 - GCP:
metadata.google.internal、169.254.169.254、http://metadata - Azure:
169.254.169.254 - DigitalOcean:
169.254.169.254
実装チェックリスト
- URL スキームが HTTP/HTTPS のみであることを検証する
- DNS を解決し、IP がプライベート/内部でないことを検証する
- クラウドメタデータ IP を明示的にブロックする
- リダイレクトの追跡を制限または無効化する
- リダイレクトを追跡する場合は、各ホップを検証する
- リクエストにタイムアウトを設定する
- レスポンスサイズを制限する
- 可能な場合はネットワーク分離を使用する
安全でないファイルアップロード
ファイルアップロードは、さまざまな攻撃を防ぐために種類、内容、サイズを検証する必要があります。
検証要件
1. ファイル種類の検証
- ファイル拡張子を許可リストと照合する
- マジックバイト/ファイルシグネチャが期待される種類と一致することを検証する
- 1 つのチェックだけに依存しない
2. ファイル内容の検証
- マジックバイトを読み取って検証する
- 画像の場合: 画像ライブラリで処理を試みる(不正な形式のファイルを検出できる)
- ドキュメントの場合: マクロや埋め込みオブジェクトをスキャンする
- ポリグロットファイル(複数の種類として有効なファイル)をチェックする
3. ファイルサイズの制限
- サーバー側で最大ファイルサイズを設定する
- Web サーバー/プロキシの制限も設定する
- ファイル種類ごとの制限を検討する(画像は動画より小さく)
一般的なバイパスと攻撃
| 攻撃 | 説明 | 防止策 |
|---|---|---|
| 拡張子バイパス | shell.php.jpg | 完全な拡張子をチェックし、許可リストを使用する |
| ヌルバイト | shell.php%00.jpg | ファイル名をサニタイズし、ヌルバイトをチェックする |
| 二重拡張子 | shell.jpg.php | 単一の拡張子のみを許可する |
| MIME タイプのなりすまし | Content-Type を image/jpeg に設定する | マジックバイトを検証する |
| マジックバイト注入 | 悪意のあるファイルの先頭に有効なマジックバイトを付加する | ヘッダーだけでなくファイル構造全体をチェックする |
| ポリグロットファイル | JPEG と JavaScript の両方として有効なファイル | 期待される種類としてファイルを解析し、無効なら拒否する |
| JavaScript を含む SVG | <svg onload="alert(1)"> | SVG をサニタイズするか、完全に禁止する |
| ファイルアップロード経由の XXE | 悪意のある DOCX、XLSX(これらは XML) | パーサーで外部エンティティを無効化する |
| ZIP slip | アーカイブ内の ../../../etc/passwd | 展開されたパスを検証する |
| ImageMagick の脆弱性悪用 | 巧妙に細工された画像 | ImageMagick を最新に保ち、policy.xml を使用する |
| ファイル名インジェクション | ファイル名に ; rm -rf / | ファイル名をサニタイズし、ランダムな名前を使用する |
| Content-type の混乱 | ブラウザの MIME スニッフィング | X-Content-Type-Options: nosniff を設定する |
マジックバイト早見表
| 種類 | マジックバイト(16 進数) |
|---|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 0D 0A 1A 0A |
| GIF | 47 49 46 38 |
25 50 44 46 | |
| ZIP | 50 4B 03 04 |
| DOCX/XLSX | 50 4B 03 04(ZIP ベース) |
安全なアップロード処理
- ファイル名を変更する:ランダムな UUID 名を使用し、元の名前は破棄します
- Web ルートの外部に保存する:またはアップロード用に別のドメインを使用します
- 適切なヘッダーを付けて配信する:
Content-Disposition: attachment(ダウンロードを強制します)X-Content-Type-Options: nosniff- 実際のファイル形式に一致する
Content-Type
- CDN/別のドメインを使用する:アップロードされたコンテンツをメインアプリから分離します
- 制限的な権限を設定する:アップロードされたファイルを実行可能にしないでください
SQL インジェクション
SQL インジェクションは、適切に処理されずにユーザー入力が SQL クエリに組み込まれると発生します。
防止方法
1. パラメーター化クエリ(プリペアドステートメント) — 主な防御策
-- VULNERABLE
query = "SELECT * FROM users WHERE id = " + userId
-- SECURE
query = "SELECT * FROM users WHERE id = ?"
execute(query, [userId])
2. ORM の使用
- 自動的にパラメーター化する ORM メソッドを使用します
- ORM の生クエリメソッドには注意してください
- ORM 固有のインジェクションポイントに注意してください
3. 入力の検証
- データ型を検証します(整数は整数である必要があります)
- 該当する場合は、許可する値をホワイトリストで指定します
- これは多層防御であり、主な防御策ではありません
注意すべきインジェクションポイント
- WHERE 句
- ORDER BY 句(見落とされがちです。パラメーターを使用できないため、ホワイトリストで指定する必要があります)
- LIMIT/OFFSET の値
- テーブル名とカラム名(パラメーター化できないため、ホワイトリストで指定する必要があります)
- INSERT の値
- UPDATE SET の値
- 動的なリストを含む IN 句
- LIKE パターン(ワイルドカード「%」「_」もエスケープします)
その他の防御策
- 最小権限の原則:データベースユーザーには、必要最小限の権限を付与します
- 危険な関数を無効にする:SQL Server の
xp_cmdshellなど - エラー処理:SQL エラーをユーザーに決して公開しないでください
XML 外部エンティティ(XXE)
XXE の脆弱性は、XML パーサーがユーザー提供の XML 内にある外部エンティティ参照を処理すると発生します。
脆弱になり得る状況
XML の直接入力:
- SOAP API
- XML-RPC
- XML ファイルのアップロード
- 設定ファイルの解析
- RSS/Atom フィードの処理
XML の間接的な使用:
- JSON/その他の形式をサーバー側で XML に変換する場合
- Office ドキュメント(DOCX、XLSX、PPTX は XML を含む ZIP です)
- SVG ファイル(XML ベース)
- SAML アサーション
- XFA フォームを含む PDF
言語/パーサー別の防止策
Java:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setExpandEntityReferences(false);
Python(lxml):
from lxml import etree
parser = etree.XMLParser(resolve_entities=False, no_network=True)
# Or use defusedxml library
PHP:
libxml_disable_entity_loader(true);
// Or use XMLReader with proper settings
Node.js:
// Use libraries that disable DTD processing by default
// If using libxmljs, set { noent: false, dtdload: false }
.NET:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
settings.XmlResolver = null;
XXE 防止チェックリスト
- 可能であれば、DTD 処理を完全に無効にします
- 外部エンティティの解決を無効にします
- 外部 DTD の読み込みを無効にします
- XInclude 処理を無効にします
- 最新のパッチが適用された XML パーサーのバージョンを使用します
- DTD が必要な場合は、解析前に XML を検証/サニタイズします
- 可能であれば、XML の代わりに JSON の使用を検討します
パストラバーサル
パストラバーサルの脆弱性は、ユーザー入力によってファイルパスが制御され、意図したディレクトリの外部にあるファイルへアクセスできる場合に発生します。
脆弱になりやすいパターン
# VULNERABLE
file_path = "/uploads/" + user_input
file_path = base_dir + request.params['file']
template = "templates/" + user_provided_template
防止策
1. パスにユーザー入力を使用しない
# Instead of using user input directly
# Use indirect references
files = {'report': '/reports/q1.pdf', 'invoice': '/invoices/2024.pdf'}
file_path = files.get(user_input) # Returns None if invalid
2. 正規化と検証
import os
def safe_join(base_directory, user_path):
# Ensure base is absolute and normalized
base = os.path.abspath(os.path.realpath(base_directory))
# Join and then resolve the result
target = os.path.abspath(os.path.realpath(os.path.join(base, user_path)))
# Ensure the commonpath is the base directory
if os.path.commonpath([base, target]) != base:
raise ValueError("Error!")
return target
3. 入力のサニタイズ
..の並びを削除または拒否します- 絶対パスを示す記号(
/、C:)を削除または拒否します - 許可する文字をホワイトリストで指定します(英数字、ハイフン、アンダースコア)
- 該当する場合は、ファイル拡張子を検証します
パストラバーサルのチェックリスト
- ファイルパスにユーザー入力を直接使用しないでください
- パスを正規化し、ベースディレクトリに対して検証します
- 該当する場合は、ファイル拡張子を制限します
- さまざまなエンコード方法や回避手法を使ってテストします
セキュリティヘッダーのチェックリスト
すべてのレスポンスに次のヘッダーを含めます。
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: [see XSS section]
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Cache-Control: no-store (for sensitive pages)
JWT のセキュリティ
JWT の設定ミスは、認証の完全な回避やトークンの偽造につながる可能性があります。
脆弱性
| 脆弱性 | 防止策 |
|---|---|
alg: none 攻撃 | アルゴリズムを必ずサーバー側で検証し、none を拒否します |
| アルゴリズムの混同 | 期待するアルゴリズムを明示的に指定し、トークンから導出しないでください |
| 弱い HMAC シークレット | 256 ビット以上の暗号学的に安全なランダムシークレットを使用します |
| 有効期限の欠落 | exp クレームを必ず設定します |
| localStorage 内のトークン | httpOnly、Secure、SameSite=Strict 属性を設定した Cookie に保存し、localStorage には決して保存しないでください |
セキュアな実装
// 1. SIGNING
// Always use environment variables for secrets
const secret = process.env.JWT_SECRET;
const token = jwt.sign({
sub: userId,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (15 * 60), // 15 mins (Short-lived)
jti: crypto.randomUUID() // Unique ID for revocation/blacklisting
}, secret, {
algorithm: 'HS256'
});
// 2. SENDING (Cookie Best Practices)
// Protect against XSS and CSRF
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict'
});
// 3. VERIFYING
// CRITICAL: Whitelist the allowed algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] }, (err, decoded) => {
if (err) {
// Handle invalid token
}
// Trust the payload
});
JWT チェックリスト
- 検証時にアルゴリズムを明示的に指定する(トークンヘッダーを決して信用しない)
-
alg: noneを拒否する - シークレットには256ビット以上のランダムデータを使用する(パスワードやフレーズは使用しない)
-
expクレームを必ず設定し、検証する - トークンは httpOnly Cookie に保存する(localStorage/sessionStorage は使用しない)
- リフレッシュトークンのローテーションを実装する(使用時に古いリフレッシュトークンを無効化する)
API セキュリティ
マスアサインメント
フィルタリングしていないリクエストボディを受け入れると、権限昇格につながる可能性があります。
// VULNERABLE — user can set { role: "admin" } in request body
User.update(req.body)
// SECURE — whitelist allowed fields
const allowed = ['name', 'email', 'avatar']
const updates = pick(req.body, allowed)
User.update(updates)
これはあらゆる ORM/フレームワークに当てはまります。リクエストで変更可能なフィールドは、必ず明示的に定義してください。
GraphQL
| 脆弱性 | 防止策 |
|---|---|
| 本番環境でのイントロスペクション | 本番環境ではイントロスペクションを無効にします。 |
| クエリ深度攻撃 | クエリ深度制限(例:最大10階層)を実装します。 |
| クエリ複雑度攻撃 | 厳格なクエリコスト制限を計算し、適用します。 |
| バッチ処理攻撃 | 1つのリクエストで許可する操作数を制限します。 |
const server = new ApolloServer({
introspection: process.env.NODE_ENV !== 'production',
validationRules: [
depthLimit(10),
costAnalysis({ maximumCost: 1000 })
]
})
一般的なセキュリティ原則
コードを生成する際は、常に次の点に従ってください。
- すべての入力をサーバー側で検証する — クライアント側の検証だけを決して信用しない
- パラメーター化クエリを使用する — ユーザー入力をクエリに決して連結しない
- コンテキストに応じて出力をエンコードする — HTML、JS、URL、CSS の各コンテキストには、それぞれ異なるエンコードが必要です
- 認証チェックを適用する — ルーティング時だけでなく、すべてのエンドポイントで実施する
- 認可チェックを適用する — ユーザーが特定のリソースにアクセスできることを確認する
- 安全なデフォルト設定を使用する
- エラーを安全に処理する — スタックトレースや内部情報をユーザーに漏らさない
- 依存関係を最新の状態に保つ — 脆弱な依存関係を追跡するツールを使用する
判断に迷う場合は、より制限が厳しく安全な選択肢を選び、セキュリティ上の考慮事項をコメントに記録してください。