diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 006196b..b1e20ee 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,14 +7,14 @@ }, "metadata": { "description": "AI-driven development toolkit for TDD and SDD workflows, providing comprehensive command templates and agents to enhance developer productivity with Claude Code", - "version": "1.3.0" + "version": "1.4.0" }, "plugins": [ { "name": "tsumiki", "source": "./", "description": "AI-driven development toolkit for TDD and SDD workflows, providing comprehensive command templates and agents to enhance developer productivity with Claude Code", - "version": "1.3.0", + "version": "1.4.0", "author": { "name": "makoto kuroeda", "email": "kuroeda.makoto@classmethod.jp" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index ce341c0..aa88f31 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "tsumiki", - "version": "1.3.0", + "version": "1.4.0", "description": "AI-driven development toolkit for TDD and SDD workflows, providing comprehensive command templates and agents to enhance developer productivity with Claude Code", "author": { "name": "makoto kuroeda", diff --git a/MANUAL.md b/MANUAL.md index cdc7663..b1735ca 100644 --- a/MANUAL.md +++ b/MANUAL.md @@ -395,6 +395,86 @@ tsumikiの利用可能なコマンド一覧の表示、個別コマンドの詳 /tsumiki:timeout-fix ``` +### セキュリティチェックスキル + +#### ipa-security-check + +IPA(情報処理推進機構)が公開する以下5つの公式資料に基づき、ソースコード・設定ファイルを静的検査して脆弱性候補を検出するスキルです。検出結果には必ず IPA 原典の出典(文書名・章・ページ・URL)が付与されます。 + +**準拠する IPA 資料**: + +| 略称 | 正式名称 | 主な検査内容 | +|---|---|---| +| SWS | 安全なウェブサイトの作り方 改訂第7版 | 11脆弱性(SQLi / OSコマンド / トラバーサル / セッション / XSS / CSRF / HTTPヘッダ / メールヘッダ / クリックジャッキング / BoF / アクセス制御) | +| SQL | 安全なSQLの呼び出し方 | プレースホルダ使い分け・LIKE述語・識別子検証・文字コード問題 | +| WHC | ウェブ健康診断仕様 | 13診断項目のうち静的解析でカバー可能な観点 | +| OPS | 安全なウェブサイトの運用管理に向けての20ヶ条 | HTTPヘッダ・依存ライブラリ・設定ファイル類 | +| CL | セキュリティ実装チェックリスト | 改訂第7版 p.105-108 のチェックリスト | + +**起動方法**: + +```bash +# カレントディレクトリ全体をスキャン +/tsumiki:ipa-security-check + +# 指定パス / glob のみ +/tsumiki:ipa-security-check src/ +/tsumiki:ipa-security-check **/*.php + +# 現ブランチと main の差分ファイルのみ +/tsumiki:ipa-security-check --diff + +# カテゴリ限定 +/tsumiki:ipa-security-check --categories sqli,xss src/ + +# 重大度フィルタ +/tsumiki:ipa-security-check --severity high + +# 出力ファイル指定 +/tsumiki:ipa-security-check --output report.md,report.sarif +``` + +自然文(「IPA のセキュリティチェックをして」など)でも起動できます。 + +**対応言語**: PHP / Java(`.java`, `.jsp`) / Ruby(`.rb`, `.erb`) / Python / JavaScript / TypeScript(`.vue`含む) / C# / .NET(`.cshtml`, `.aspx`含む) / Go / 各種設定ファイル(`nginx.conf`, `.htaccess`, `web.xml`, `*.yaml`, `Dockerfile` など) + +**生成されるファイル**: +- `./ipa-security-report.md` - Markdown 形式のレポート +- `./ipa-security-report.sarif` - SARIF 2.1.0 形式のレポート +- `.tmp/ipa-security-check/` - 中間ファイル + +**トリアージ機能**: + +検出結果は Markdown レポート内に保持される `` ブロックで状態管理できます。`snippet_hash`(rule_id + file + 正規化された code_snippet の sha256)で同定するため、行番号が変動しても次回スキャンで状態を引き継げます。 + +| ステータス | 意味 | +|---|---| +| `未対応` | 未着手(新規 finding のデフォルト) | +| `対応する` | 修正予定 / 実施中 | +| `問題なし` | 確認の上、本物の脆弱性ではない(サマリから除外) | +| `保留` | 一旦保留(サマリから除外) | + +**誤検知のインライン抑止**: ソースコードに以下のマーカーを置くと、検出段階で finding を生成しません。 + +``` +// ipa-skip: IPA-SWS-1-SQLI-001 reason: 内部固定値を埋め込んでいるため +``` + +**特徴**: +- 14体の検査エージェント(SQLi / XSS / CSRF 等)を並列起動して高速検査 +- 偽陽性レビューエージェントが周辺コード・呼び出し元を再評価して False Positive を識別 +- すべての検出結果に IPA 原典の `document / section / page / url` を必須付与 + +#### ipa-security-guide + +`ipa-security-check` が出力したレポートを読み、各検出項目を優先順位付きの `tsumiki:dev-debug` 依頼リストに変換するスキルです。対象プロジェクトの言語・FW を問わず汎用的に使用できます。 + +```bash +/tsumiki:ipa-security-guide +``` + +`ipa-security-check` → `ipa-security-guide` → `tsumiki:dev-debug` の順で実行することで、脆弱性検出から修正までを一連の流れで進められます。 + ## ディレクトリ構造 ``` diff --git a/docs/external-sources.md b/docs/external-sources.md new file mode 100644 index 0000000..ef3b0f8 --- /dev/null +++ b/docs/external-sources.md @@ -0,0 +1,50 @@ +# 外部情報源 一覧 + +本リポジトリの各スキルが参照している **外部の公式資料・規格・ガイドライン** をここに集約する。 + +- スキル内部のロジック・プロンプトに反映している知見の **出典元** を明示するためのファイル。 +- 著作権・ライセンスは各情報源の発行元に帰属する。同梱の knowledge ファイル等は **二次的に整理した参照用ノート** である。 +- 各情報源の **詳細 (版情報・章立て・ページ番号など)** は、利用しているスキル内の `knowledge/_sources.md` (またはそれに相当するファイル) を参照する。 + +## 運用ルール + +新しい外部情報源を参照するスキルを追加した場合は、以下を行う: + +1. スキル内に `knowledge/_sources.md` を置き、正式名称・版・発行日・URL・ライセンス・章節などの詳細を記録する。 +2. 本ファイル (`docs/external-sources.md`) の **クイックインデックス** に1行追記する。 +3. 本ファイルの末尾に **情報源ごとのセクション** を追加し、利用スキル名・詳細リンクを記載する。 +4. スキルの finding / 出力に出典 (文書名・章・ページ・URL) を必ず添える設計にする。 + +## クイックインデックス + +| # | 情報源 (略称) | 発行者 | 利用スキル | 詳細 | +|---|---|---|---|---| +| 1 | 安全なウェブサイトの作り方 改訂第7版 ほか IPA 5 資料 | 独立行政法人 情報処理推進機構 (IPA) | `ipa-security-check` | [#1-ipa-web-security](#1-ipa-安全なウェブサイトの作り方-ほか-5-資料) | + +--- + +## 1. IPA「安全なウェブサイトの作り方」ほか 5 資料 + +| 項目 | 値 | +|---|---| +| 発行者 | 独立行政法人 情報処理推進機構 (IPA) | +| 公式紹介ページ | https://www.ipa.go.jp/security/vuln/websecurity/about.html | +| ライセンス・著作権 | IPA に帰属 | +| 利用スキル | `skills/ipa-security-check/` | +| 詳細出典マスター | [`skills/ipa-security-check/knowledge/_sources.md`](../skills/ipa-security-check/knowledge/_sources.md) | + +### 参照している資料 + +| 略称 | 正式名称 | 版 / 発行日 | URL | +|---|---|---|---| +| SWS | 安全なウェブサイトの作り方 | 改訂第7版 第4刷 / 2021-03-31 | https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017316.pdf | +| SQL | 安全なSQLの呼び出し方 (別冊) | 第1版 / 2010-03-18 | https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017320.pdf | +| WHC | ウェブ健康診断仕様 (別冊) | 第1版 第1刷 / 2012-12-26 | https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf | +| OPS | 安全なウェブサイトの運用管理に向けての20ヶ条 | - | https://www.ipa.go.jp/security/vuln/websecurity/sitecheck.html | +| CL | セキュリティ実装チェックリスト | 改訂第7版 巻末 (p.105〜108) / Excel 別添あり | https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000044403.xlsx | + +### 利用方針 + +- スキルの finding 出力には、必ず IPA 原典の `document / section / page / url` を添える。 +- 提案は「**根本的解決**」と「**保険的対策**」を区別して提示する (IPA 用語に準拠)。 +- 版情報・章節・CWE 対応表など詳細は [`skills/ipa-security-check/knowledge/_sources.md`](../skills/ipa-security-check/knowledge/_sources.md) を参照。 diff --git a/skills/ipa-security-check/SKILL.md b/skills/ipa-security-check/SKILL.md new file mode 100644 index 0000000..8f3f432 --- /dev/null +++ b/skills/ipa-security-check/SKILL.md @@ -0,0 +1,199 @@ +--- +name: ipa-security-check +description: IPA「安全なウェブサイトの作り方 改訂第7版」「安全なSQLの呼び出し方」「ウェブ健康診断仕様」「セキュリティ実装チェックリスト」「安全なウェブサイトの運用管理に向けての20ヶ条」に基づき、ソースコードを静的に検査して脆弱性候補を検出する。発見した問題には IPA 原典の出典 (文書名・章・ページ・URL) を必ず付与する。 +--- + +# IPA Security Check Skill + +## このスキルが行うこと + +IPA (情報処理推進機構) が公開する以下 5 資料の指摘事項に基づき、ローカルリポジトリのソースコード・設定ファイルを静的検査し、脆弱性候補を検出する。 + +| 略称 | 正式名称 | 主な検査内容 | +|---|---|---| +| SWS | 安全なウェブサイトの作り方 改訂第7版 | 11脆弱性 (SQLi/OSコマンド/トラバーサル/セッション/XSS/CSRF/HTTPヘッダ/メールヘッダ/クリックジャッキング/BoF/アクセス制御) | +| SQL | 安全なSQLの呼び出し方 | プレースホルダ使い分け・LIKE述語・識別子検証・文字コード問題 | +| WHC | ウェブ健康診断仕様 | 13診断項目のうち静的解析でカバー可能な観点 | +| OPS | 安全なウェブサイトの運用管理に向けての20ヶ条 | HTTPヘッダ・依存ライブラリ・設定ファイル類 | +| CL | セキュリティ実装チェックリスト | 改訂第7版 p.105-108 のチェックリスト | + +すべての検出結果に IPA 原典の `document / section / page / url` を必ず添えて返す。 + +## 起動方法 + +スラッシュコマンド `/ipa-security-check` で起動する。引数で対象スコープを指定する。 + +| 形式 | 動作 | +|---|---| +| `/ipa-security-check` | カレント WD 全体をスキャン | +| `/ipa-security-check ` | 指定パス/glob のみ (例: `src/`, `**/*.php`) | +| `/ipa-security-check --diff` | 現ブランチと `main` の差分ファイルのみ | +| `/ipa-security-check --categories sqli,xss ` | カテゴリ限定 | +| `/ipa-security-check --severity high` | High 以上のみ | +| `/ipa-security-check --output report.md,report.sarif` | 出力ファイル指定 | + +自然文 (「IPA のセキュリティチェックをして」など) でも起動する。 + +## 実行手順 (Claude が行うこと) + +1. **`lib/scope_resolver.md`** を読み、引数を解釈して対象ファイル一覧と言語マッピングを作る +2. **`lib/shard_planner.md`** を読み、カテゴリごとにファイル数を数え、閾値超過時は N 分割する +3. **`agents/` 配下** の検査系サブエージェント (14 体) を **メインから並列起動** する (公式制約によりサブエージェントから二次サブエージェントは起動できないため、分割はメイン側で行う) +4. 各サブエージェントは `findings[]` を含む JSON を返す。メインは findings のみ集約してコード本文は文脈に保持せず、`.tmp/ipa-security-check/findings_raw.json` に書き出す +5. **Phase 5 (偽陽性レビュー)**: + - `scripts/snippet_hash.py` を Bash で実行して `findings_with_hash.json` を作る + - `15-false-positive-review` エージェントを **5 件 / shard** で並列起動し、返ってきた `verdicts[]` を結合して `verdicts.json` に保存 +6. **Phase 6 + 出力 (Step 7)**: `scripts/render_report.py` を Bash で実行する。スクリプトが内部で以下を行う: + - verdict を `snippet_hash` で findings にマージ + - 既存 Markdown レポートから triage ブロックを抽出 (`lib/triage_state.md` 準拠) し、`snippet_hash` 一致で新 findings にステータス引き継ぎ + - `templates/report.md.tmpl` を埋めて Markdown を出力、SARIF 2.1.0 も同時生成 +7. デフォルトの保存先は `./ipa-security-report.md` と `./ipa-security-report.sarif` (`--output` で上書き) + +中間ファイルの作業ディレクトリは `.tmp/ipa-security-check/` (リポジトリルート直下、名前に `tmp` を含めること)。 + +詳細な分配ロジックは **`lib/orchestrator.md`** に従う。Claude は `lib/orchestrator.md` を読んでそのとおりに動くこと。 + +## 対応言語 + +| 言語 | 拡張子 | +|---|---| +| PHP | `.php` | +| Java | `.java`, `.jsp` | +| Ruby | `.rb`, `.erb` | +| Python | `.py` | +| JavaScript / TypeScript | `.js`, `.jsx`, `.ts`, `.tsx`, `.vue` | +| C# / .NET | `.cs`, `.cshtml`, `.aspx` | +| Go | `.go` | +| 設定ファイル | `.conf`, `nginx.conf`, `.htaccess`, `web.xml`, `*.yaml`, `*.yml`, `Dockerfile` | + +## トリアージ (ステータス管理) + +検出結果には 4 ステータスを管理できる。状態は Markdown レポート内の HTML コメントブロックに保持され、次回スキャン時に `snippet_hash` で引き継がれる。詳細は `lib/triage_state.md`。 + +| ステータス | 意味 | 次回スキャンでの扱い | +|---|---|---| +| `未対応` | 未着手 (新規 finding のデフォルト) | 通常表示 | +| `対応する` | 修正予定 / 実施中 | 通常表示 | +| `問題なし` | 確認の上、本物の脆弱性ではない | `## トリアージ済み (抑止)` セクションへ移動。サマリから除外 | +| `保留` | 一旦保留 | `## トリアージ済み (抑止)` セクションへ移動。サマリから除外 | + +ユーザーは各 finding 直下の `` ブロックの `status:` と `note:` を編集する。 +`snippet_hash` は `rule_id + file + 正規化された code_snippet` の sha256 で計算するため、行番号が変動しても引き継げる。 + +### 偽陽性候補 + +`15-false-positive-review` エージェントが周辺コード/呼び出し元を再評価して `likely_false_positive` と判定した finding は `## 偽陽性候補` セクションへ移動 (本文の検出結果からは除外)。 +誤判定と思う場合は対応する triage ブロックの `status:` を `対応する` に変更すると次回スキャンで通常レポートに戻る。 + +## 誤検知のインライン抑止 (補助) + +ソースコードに以下のインラインマーカーを置くと検出段階で finding を生成しない (トリアージとは別軸)。 + +``` +// ipa-skip: IPA-SWS-1-SQLI-001 reason: 内部固定値を埋め込んでいるため +``` + +`reason:` は必須。 + +## サブエージェント一覧 + +`agents/` 配下に 15 体定義。 + +### 検査エージェント (14 体): Phase 1〜4 で並列起動 + +| エージェント | 担当 | +|---|---| +| `01-sql-injection` | SQL インジェクション | +| `02-os-command-injection` | OS コマンドインジェクション | +| `03-directory-traversal` | ディレクトリトラバーサル | +| `04-session-management` | セッション管理の不備 | +| `05-xss` | クロスサイトスクリプティング | +| `06-csrf` | クロスサイトリクエストフォージェリ | +| `07-http-header-injection` | HTTP ヘッダインジェクション | +| `08-mail-header-injection` | メールヘッダインジェクション | +| `09-clickjacking` | クリックジャッキング | +| `10-buffer-overflow` | バッファオーバーフロー | +| `11-access-control` | アクセス制御の不備 | +| `safe-sql-details` | 安全な SQL の呼び出し方 深掘り | +| `web-health-check` | ウェブ健康診断 静的解析項目 | +| `operation-checklist` | 運用 20ヶ条 + 実装チェックリスト | + +### レビューエージェント (1 体): Phase 5 で並列起動 + +| エージェント | 担当 | +|---|---| +| `15-false-positive-review` | 検査エージェントの findings に対し周辺コード/呼び出し元を再 Read して偽陽性候補を識別 | + +## 出力契約 (検査サブエージェント → メイン) + +検査系 (01〜11 / safe-sql-details / web-health-check / operation-checklist) は以下の JSON 形式のみ返す。コード本文や中間ログは返さない。 + +```json +{ + "agent": "sql-injection", + "files_scanned": 142, + "findings": [ + { + "rule_id": "IPA-SWS-1-SQLI-001", + "severity": "critical", + "category": "sql_injection", + "file": "src/users.php", + "line": 45, + "column": 12, + "code_snippet": "$sql = \"SELECT * FROM users WHERE id = \" . $_GET['id'];", + "message": "...", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.1 SQLインジェクション", + "page": "6-12", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "プレースホルダによる SQL 文の組み立て", + "cwe": "CWE-89", + "fix_example": "..." + } + ], + "errors": [] +} +``` + +> `snippet_hash` / `fp_verdict` / `status` などのトリアージ系フィールドは orchestrator (Phase 5・Phase 6) が後付けする。検査エージェントは付与しない。 + +`15-false-positive-review` の出力契約は `agents/15-false-positive-review.md` を参照。 + +## ファイル構成 + +``` +.claude/skills/ipa-security-check/ +├── SKILL.md ← 本ファイル (Claude が最初に読む) +├── commands/ +│ └── ipa-security-check.md +├── agents/ ← 15 体のサブエージェント定義 +│ ├── 01〜11, safe-sql-details, web-health-check, operation-checklist (検査 14 体) +│ └── 15-false-positive-review.md (偽陽性レビュー) +├── knowledge/ ← IPA 原文ベースの知識 (Skill 単独配布で完結) +├── rules/ ← YAML 検出シグネチャ +├── templates/ ← Markdown / SARIF テンプレート +├── scripts/ ← Bash 経由で呼ぶ Python 実装 +│ ├── snippet_hash.py ← snippet_hash 計算 (Phase 5 入力準備) +│ └── render_report.py ← verdict/triage マージ + Markdown/SARIF 出力 (Step 7) +└── lib/ + ├── orchestrator.md + ├── scope_resolver.md + ├── shard_planner.md + ├── triage_state.md ← ステータス保持・引き継ぎ仕様 + └── output_formatter.md +``` + +実行時の中間ファイルは `.tmp/ipa-security-check/` 配下に置く (作業ディレクトリ名は必ず `tmp` を含める)。 + +## 重要な原則 + +- **IPA 原典への出典明記**: すべての finding に `ipa.document / section / page / url` を必須付与 +- **Skill 単独配布**: `knowledge/` `rules/` を Skill 内に同梱。外部の `docs/` を参照しない +- **メイン文脈の節約**: サブエージェントは findings / verdicts JSON のみ返却 +- **ネスト禁止準拠**: サブエージェントは二次サブエージェントを起動しない。分割はメイン側で行う +- **偽陽性レビューの分離**: 検出フェーズと FP 判定フェーズは分離する。FP 判定は周辺コードを再 Read して行う +- **トリアージ状態は Markdown 内に保持**: 専用状態ファイルは作らず、レポート自体が状態を持つ (Git で履歴管理可) +- **完全一致抑止**: `snippet_hash` (rule_id + file + 正規化された code_snippet の sha256) で同定する。行番号変動には耐える diff --git a/skills/ipa-security-check/agents/01-sql-injection.md b/skills/ipa-security-check/agents/01-sql-injection.md new file mode 100644 index 0000000..c9e8ab0 --- /dev/null +++ b/skills/ipa-security-check/agents/01-sql-injection.md @@ -0,0 +1,84 @@ +--- +name: 01-sql-injection +description: SQL インジェクションの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 01-sql-injection サブエージェント + +## 担当 +SQL インジェクションの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.1) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/sql_injection.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/01_sql_injection.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "01-sql-injection", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-SQLI-001", + "severity": "critical", + "category": "sql_injection", + "file": "src/users.php", + "line": 45, + "column": 12, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.1 SQLインジェクション", + "page": "6-12", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "プレースホルダによる SQL 文の組み立て", + "cwe": "CWE-89", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- ORM の安全な使い方 (パラメタライズドクエリ確認済み) +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/02-os-command-injection.md b/skills/ipa-security-check/agents/02-os-command-injection.md new file mode 100644 index 0000000..e1b6c8b --- /dev/null +++ b/skills/ipa-security-check/agents/02-os-command-injection.md @@ -0,0 +1,84 @@ +--- +name: 02-os-command-injection +description: OS コマンドインジェクションの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 02-os-command-injection サブエージェント + +## 担当 +OS コマンドインジェクションの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.2) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/os_command_injection.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/02_os_command_injection.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "02-os-command-injection", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-OSCMD-001", + "severity": "critical", + "category": "os_command_injection", + "file": "src/exec.php", + "line": 23, + "column": 8, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.2 OSコマンドインジェクション", + "page": "13-17", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "シェルを経由しない API の利用 / 入力値の検証", + "cwe": "CWE-78", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- shell=False / 配列形式の安全な exec 呼び出し +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/03-directory-traversal.md b/skills/ipa-security-check/agents/03-directory-traversal.md new file mode 100644 index 0000000..389dc1e --- /dev/null +++ b/skills/ipa-security-check/agents/03-directory-traversal.md @@ -0,0 +1,85 @@ +--- +name: 03-directory-traversal +description: ディレクトリトラバーサルの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 03-directory-traversal サブエージェント + +## 担当 +ディレクトリトラバーサルの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.3) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/directory_traversal.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/03_directory_traversal.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "03-directory-traversal", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-DT-001", + "severity": "high", + "category": "directory_traversal", + "file": "src/download.php", + "line": 12, + "column": 5, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル", + "page": "18-22", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "外部からのパス名指定を排除 / basename 等でファイル名のみを抽出", + "cwe": "CWE-22", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- ホワイトリストによるファイル名検証済みコード +- realpath() でディレクトリ境界が確認されている処理 +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/04-session-management.md b/skills/ipa-security-check/agents/04-session-management.md new file mode 100644 index 0000000..f5c7d1b --- /dev/null +++ b/skills/ipa-security-check/agents/04-session-management.md @@ -0,0 +1,84 @@ +--- +name: 04-session-management +description: セッション管理の不備の静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 04-session-management サブエージェント + +## 担当 +セッション管理の不備の静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.4) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/session_management.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/04_session_management.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "04-session-management", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-SESS-001", + "severity": "high", + "category": "session_management", + "file": "src/login.php", + "line": 30, + "column": 4, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.4 セッション管理の不備", + "page": "23-30", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "ログイン成功後にセッション ID を再発行 / Secure/HttpOnly 属性付与", + "cwe": "CWE-384", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- フレームワーク側で安全なセッション管理が行われている場合 +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/05-xss.md b/skills/ipa-security-check/agents/05-xss.md new file mode 100644 index 0000000..3bd1edf --- /dev/null +++ b/skills/ipa-security-check/agents/05-xss.md @@ -0,0 +1,85 @@ +--- +name: 05-xss +description: クロスサイトスクリプティング (XSS) の静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 05-xss サブエージェント + +## 担当 +クロスサイトスクリプティング (XSS) の静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.5) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/xss.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/05_xss.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "05-xss", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-XSS-001", + "severity": "high", + "category": "xss", + "file": "src/profile.php", + "line": 67, + "column": 10, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.5 クロスサイト・スクリプティング", + "page": "31-43", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "出力時に HTML エスケープ / 属性値はダブルクォートで囲む / URL はホワイトリストで検証", + "cwe": "CWE-79", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- React/Vue 等で自動エスケープされる文字列補間 (ただし `dangerouslySetInnerHTML` / `v-html` は finding に含める) +- 既にエスケープ済み変数 (htmlspecialchars / escapeHtml / e() などの戻り値) +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/06-csrf.md b/skills/ipa-security-check/agents/06-csrf.md new file mode 100644 index 0000000..3aeb0dd --- /dev/null +++ b/skills/ipa-security-check/agents/06-csrf.md @@ -0,0 +1,85 @@ +--- +name: 06-csrf +description: クロスサイトリクエストフォージェリ (CSRF) の静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 06-csrf サブエージェント + +## 担当 +クロスサイトリクエストフォージェリ (CSRF) の静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.6) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/csrf.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/06_csrf.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "06-csrf", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-CSRF-001", + "severity": "high", + "category": "csrf", + "file": "src/transfer.php", + "line": 15, + "column": 3, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.6 CSRF(クロスサイト・リクエスト・フォージェリ)", + "page": "44-50", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "重要処理に専用トークンを発行/検証 / Referer 確認 / 再認証", + "cwe": "CWE-352", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- フレームワークの CSRF 保護機能 (csrf_field, @csrf, csrf_token 等) が有効な箇所 +- SameSite=Strict/Lax 設定済み + 安全なメソッドのみのエンドポイント +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/07-http-header-injection.md b/skills/ipa-security-check/agents/07-http-header-injection.md new file mode 100644 index 0000000..1816c5c --- /dev/null +++ b/skills/ipa-security-check/agents/07-http-header-injection.md @@ -0,0 +1,85 @@ +--- +name: 07-http-header-injection +description: HTTP ヘッダインジェクションの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 07-http-header-injection サブエージェント + +## 担当 +HTTP ヘッダインジェクションの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.7) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/http_header_injection.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/07_http_header_injection.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "07-http-header-injection", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-HHI-001", + "severity": "high", + "category": "http_header_injection", + "file": "src/redirect.php", + "line": 8, + "column": 4, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.7 HTTPヘッダ・インジェクション", + "page": "51-56", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "ヘッダ生成 API の使用 / 改行コード (CR LF) の除去・拒否", + "cwe": "CWE-113", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 固定文字列でヘッダ値を組み立てているケース +- フレームワークの安全なヘッダ生成 API を使用しているケース +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/08-mail-header-injection.md b/skills/ipa-security-check/agents/08-mail-header-injection.md new file mode 100644 index 0000000..540c3ac --- /dev/null +++ b/skills/ipa-security-check/agents/08-mail-header-injection.md @@ -0,0 +1,84 @@ +--- +name: 08-mail-header-injection +description: メールヘッダインジェクションの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 08-mail-header-injection サブエージェント + +## 担当 +メールヘッダインジェクションの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.8) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/mail_header_injection.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/08_mail_header_injection.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "08-mail-header-injection", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-MHI-001", + "severity": "medium", + "category": "mail_header_injection", + "file": "src/contact.php", + "line": 25, + "column": 6, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.8 メールヘッダ・インジェクション", + "page": "57-61", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "メール送信用ライブラリの使用 / ヘッダ用入力値から改行を除去", + "cwe": "CWE-93", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 信頼できるメール送信ライブラリ (PHPMailer/Nodemailer/JavaMail 等) の安全な API を経由するケース +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/09-clickjacking.md b/skills/ipa-security-check/agents/09-clickjacking.md new file mode 100644 index 0000000..624a4e7 --- /dev/null +++ b/skills/ipa-security-check/agents/09-clickjacking.md @@ -0,0 +1,84 @@ +--- +name: 09-clickjacking +description: クリックジャッキングの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 09-clickjacking サブエージェント + +## 担当 +クリックジャッキングの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.9) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/clickjacking.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/09_clickjacking.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "09-clickjacking", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-CJ-001", + "severity": "medium", + "category": "clickjacking", + "file": "config/nginx.conf", + "line": 42, + "column": 1, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.9 クリックジャッキング", + "page": "62-66", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "X-Frame-Options: DENY/SAMEORIGIN または CSP frame-ancestors の設定", + "cwe": "CWE-1021", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- X-Frame-Options もしくは CSP frame-ancestors が設定済みの応答 +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/10-buffer-overflow.md b/skills/ipa-security-check/agents/10-buffer-overflow.md new file mode 100644 index 0000000..a7e51cc --- /dev/null +++ b/skills/ipa-security-check/agents/10-buffer-overflow.md @@ -0,0 +1,85 @@ +--- +name: 10-buffer-overflow +description: バッファオーバーフローの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 10-buffer-overflow サブエージェント + +## 担当 +バッファオーバーフローの静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.10) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/buffer_overflow.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/10_buffer_overflow.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "10-buffer-overflow", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-BOF-001", + "severity": "medium", + "category": "buffer_overflow", + "file": "src/parser.c", + "line": 88, + "column": 5, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.10 バッファオーバーフロー", + "page": "67-71", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "バッファ長を意識した API の使用 (strncpy_s 等) / 直接バッファ操作を避ける", + "cwe": "CWE-120", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 既に長さ検証済みのバッファ操作 +- Go の unsafe パッケージを使っていない通常の slice 操作 (検出対象外) +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/11-access-control.md b/skills/ipa-security-check/agents/11-access-control.md new file mode 100644 index 0000000..e7c59f6 --- /dev/null +++ b/skills/ipa-security-check/agents/11-access-control.md @@ -0,0 +1,85 @@ +--- +name: 11-access-control +description: アクセス制御の不備の静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 11-access-control サブエージェント + +## 担当 +アクセス制御の不備の静的検査 (IPA 安全なウェブサイトの作り方 改訂第7版 1.11) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/access_control.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/11_access_control.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "11-access-control", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SWS-1-AC-001", + "severity": "high", + "category": "access_control", + "file": "src/admin.php", + "line": 5, + "column": 1, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの作り方 改訂第7版", + "section": "1.11 アクセス制御や認可制御の欠落", + "page": "72-76", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "要保護リソースに認可チェックを必ず実装 / IDOR 対策 (本人所有か検証)", + "cwe": "CWE-284", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- ミドルウェアやデコレータ (`@login_required`, `auth` middleware 等) で保護されているハンドラ +- 公開エンドポイント (ログイン画面/ヘルスチェック等) として明示されている箇所 +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/15-false-positive-review.md b/skills/ipa-security-check/agents/15-false-positive-review.md new file mode 100644 index 0000000..8578828 --- /dev/null +++ b/skills/ipa-security-check/agents/15-false-positive-review.md @@ -0,0 +1,126 @@ +--- +name: 15-false-positive-review +description: 既存 finding に対して周辺コード/呼び出し元を再評価し、偽陽性候補を判定するサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# 15-false-positive-review サブエージェント + +## 担当 + +Phase 1〜4 の各検査エージェントが返した findings[] に対し、**周辺コードと呼び出し元を再 Read** して偽陽性候補を識別する。 +このエージェントは新規に finding を増やすことはせず、既存 finding に対して `verdict` (判定) と `reason` (根拠) を付けるのみ。 + +## 受け取る入力 + +メインからこのエージェントへは、シャード化された findings 配列が以下の形で渡される (プロンプト経由)。 + +```json +[ + { + "rule_id": "IPA-SWS-1-SQLI-001", + "file": "src/users.php", + "line": 45, + "column": 12, + "category": "sql_injection", + "severity": "critical", + "code_snippet": "$sql = \"SELECT * FROM users WHERE id = \" . $_GET['id'];", + "message": "...", + "snippet_hash": "sha256:..." + }, + ... +] +``` + +`snippet_hash` は呼び出し元 (orchestrator) が計算済み。このエージェントは触らず、そのまま出力に転写する。 + +## 動作 + +### 1. 各 finding を順に確認 + +このシャードの担当 findings を順に処理する。シャード外の finding には触れない。 + +### 2. 周辺コードを Read + +`file` を Read で開き、`line - 30` から `line + 30` までを取得する (最低 60 行)。 +ファイル末尾/先頭の場合は範囲を調整する。 + +### 3. 呼び出し元・関数定義を Grep + +該当箇所が関数/メソッド内なら以下を確認する: + +- 該当行の周囲で関数定義 (`function foo(...)`, `def foo(...)`, `public foo(...)` 等) を探す +- 関数名を特定したら、リポジトリ全体で `Grep` で呼び出し元を最大 10 件まで列挙 +- 呼び出し元のうち代表 1〜3 件について、引数に渡される値の出どころを `Read` で簡易確認 (深追いしない) + +### 4. 偽陽性判定 + +以下のいずれかに該当する場合 `verdict: "likely_false_positive"` と判定する。 +それ以外は `verdict: "true_positive"` とする。 +判定に自信が持てない場合は `verdict: "uncertain"` とし、`true_positive` 同等として扱う (報告は通常通り)。 + +#### 偽陽性パターン (代表例) + +| パターン | 説明 | +|---|---| +| 静的定数のみが渡される | 呼び出し元すべてが文字列リテラル/定数で、外部入力経路がない | +| 直前に厳格な検証あり | `is_numeric` / `ctype_digit` / `preg_match('/^\d+$/')` / 列挙チェック / ホワイトリストを通過した値のみ到達 | +| 直前に型強制あり | `(int)$x`, `intval($x)`, `parseInt(x, 10)` を経由した数値のみ到達 | +| 直前にプレースホルダバインドあり | `PDO::prepare` 直後の `bindValue(..., PDO::PARAM_INT)` 等で SQL リテラルが組み立てられていない | +| エスケープが完全 | 出力コンテキストに対し正しい関数 (`htmlspecialchars($v, ENT_QUOTES \| ENT_HTML5, 'UTF-8')` 等) を経由 | +| テストコード | `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` 配下 (orchestrator 側で severity を一段下げ済みだが、明確にダミーデータの場合は FP) | +| 配列引数 API 利用 | `exec()` ではなく `pcntl_exec($cmd, $args)` / `subprocess.run([...], shell=False)` などシェルを介さない呼び出し | +| 環境固定値 | `getenv` / `config` / `.env` 由来の固定値で、ユーザー入力が流入しない | +| サニタイズライブラリ確認済 | DOMPurify / OWASP Java Encoder / bleach 等の安全な API 経由 | + +### 5. 出力 + +最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + +```json +{ + "agent": "15-false-positive-review", + "findings_reviewed": , + "verdicts": [ + { + "snippet_hash": "sha256:...", + "rule_id": "IPA-SWS-1-SQLI-001", + "file": "src/users.php", + "line": 45, + "verdict": "true_positive", + "confidence": "high", + "reason": "$_GET['id'] が is_numeric 等の検証を経ずに SQL 文字列に連結されている。呼び出し元は controller.php:88 のみで、こちらも検証なし" + }, + { + "snippet_hash": "sha256:...", + "rule_id": "IPA-SWS-1-XSS-001", + "file": "src/page.php", + "line": 12, + "verdict": "likely_false_positive", + "confidence": "medium", + "reason": "直前で htmlspecialchars($v, ENT_QUOTES | ENT_HTML5, 'UTF-8') が適用済み。検出ルールは関数名のみで第2引数の有無を見ていない" + } + ], + "errors": [] +} +``` + +## 出力契約 (厳守) + +- `snippet_hash` は入力で渡された値をそのまま転写する (このエージェントは計算しない) +- `verdict` は `true_positive` / `likely_false_positive` / `uncertain` のいずれか +- `confidence` は `high` / `medium` / `low` のいずれか +- `reason` は日本語、1〜2 文。何を確認してそう判断したかを書く +- `findings_reviewed` は実際に判定した件数 +- JSON 以外のテキストは返さない + +## 判定の慎重さ + +- **偽陽性側に倒すコストは大きい**: 本当の脆弱性を「偽陽性」と消すと検出漏れになる +- `confidence: low` の場合は `verdict: "uncertain"` にする (= 通常通り報告される側) +- 周辺コードを読まずに `likely_false_positive` を出すことは絶対にしない + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +findings 数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/operation-checklist.md b/skills/ipa-security-check/agents/operation-checklist.md new file mode 100644 index 0000000..905dc0e --- /dev/null +++ b/skills/ipa-security-check/agents/operation-checklist.md @@ -0,0 +1,85 @@ +--- +name: operation-checklist +description: 運用 20ヶ条 + 実装チェックリストの静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# operation-checklist サブエージェント + +## 担当 +IPA「安全なウェブサイトの運用管理に向けての20ヶ条」と「セキュリティ実装チェックリスト」(改訂第7版 p.105-108) を統合的に検査する。HTTP ヘッダ・依存ライブラリ・設定ファイル類を中心に確認。 + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/operation_checklist.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/operation_checklist.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "operation-checklist", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-OPS-001", + "severity": "medium", + "category": "operation_checklist", + "file": "Dockerfile", + "line": 3, + "column": 1, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なウェブサイトの運用管理に向けての20ヶ条 / セキュリティ実装チェックリスト", + "section": "<該当節 / チェック項目>", + "page": "<該当ページ>", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/about.html" + }, + "remediation_type": "根本的解決", + "remediation": "<該当する対策>", + "cwe": "<該当 CWE>", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない +- severity は項目ごとに knowledge.md / rules.yaml の指示に従い決定する (varies) + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 既に対策済みの設定 (バージョン固定 / ハードニング済みベースイメージ 等) +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/safe-sql-details.md b/skills/ipa-security-check/agents/safe-sql-details.md new file mode 100644 index 0000000..0a0b451 --- /dev/null +++ b/skills/ipa-security-check/agents/safe-sql-details.md @@ -0,0 +1,85 @@ +--- +name: safe-sql-details +description: 安全な SQL の呼び出し方 (深掘り) の静的検査を行うサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# safe-sql-details サブエージェント + +## 担当 +IPA「安全なSQLの呼び出し方」に基づく深掘り検査 (プレースホルダの使い分け、LIKE 述語、識別子検証、文字コード問題 等) + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/safe_sql_details.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/safe_sql_details.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "safe-sql-details", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-SQL-LIKE-001", + "severity": "medium", + "category": "safe_sql_details", + "file": "src/search.php", + "line": 50, + "column": 12, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "安全なSQLの呼び出し方", + "section": "<該当節>", + "page": "<該当ページ>", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/sql.html" + }, + "remediation_type": "根本的解決", + "remediation": "プレースホルダの使い分け / LIKE のエスケープ / 識別子のホワイトリスト検証 / 接続文字コードと一致した文字列処理", + "cwe": "CWE-89", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない +- severity は問題の種別ごとに knowledge.md / rules.yaml の指示に従い決定する (varies) + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 既にエスケープ済み / プレースホルダ使用済みの呼び出し +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/agents/web-health-check.md b/skills/ipa-security-check/agents/web-health-check.md new file mode 100644 index 0000000..05f0872 --- /dev/null +++ b/skills/ipa-security-check/agents/web-health-check.md @@ -0,0 +1,85 @@ +--- +name: web-health-check +description: ウェブ健康診断仕様の静的解析項目を検査するサブエージェント +allowed_tools: [Read, Grep, Glob, Bash] +--- + +# web-health-check サブエージェント + +## 担当 +IPA「ウェブ健康診断仕様」13診断項目のうち、静的解析でカバー可能な観点を検査する + +## 動作 + +1. **検出ルールを読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/rules/web_health_check.yaml` + +2. **IPA 原文知識を読み込む** + Read で以下を読みます: + `.claude/skills/ipa-security-check/knowledge/web_health_check.md` + +3. **検査対象ファイル** + メインから受け取ったファイルパスのリストを順に検査します。 + このシャードが担当するのは渡されたファイルだけです。それ以外のファイルは見ません。 + +4. **検出手順** + - 各ファイルを Read で開く + - rules.yaml の各パターン (regex) で候補を抽出 (Grep ツールで一括検索すると効率的) + - ヒットしたコード文脈を確認し、誤検知 (テスト用ハードコード、コメント内、ライブラリ内部など) を除く + - 直前行に `ipa-skip: ` インラインマーカーがある場合は抑制 + - 確定した問題箇所のみ findings[] に積む + +5. **出力** + 最終出力は以下の JSON のみ。コード本文や中間ログは絶対に返さないこと。 + + ```json + { + "agent": "web-health-check", + "files_scanned": , + "findings": [ + { + "rule_id": "IPA-WHC-001", + "severity": "medium", + "category": "web_health_check", + "file": "config/app.yaml", + "line": 10, + "column": 1, + "code_snippet": "<該当行のコード>", + "message": "<人間可読の説明>", + "ipa": { + "document": "ウェブ健康診断仕様", + "section": "<該当診断項目>", + "page": "<該当ページ>", + "url": "https://www.ipa.go.jp/security/vuln/websecurity/healthcheck.html" + }, + "remediation_type": "根本的解決", + "remediation": "<該当する対策>", + "cwe": "<該当 CWE>", + "fix_example": "<修正例コード>" + } + ], + "errors": [] + } + ``` + +## 出力契約 (厳守) + +- `ipa.document / section / page / url` は必須。値は rules.yaml と knowledge.md から取得 +- `code_snippet` は問題行のコードのみ (前後1行程度まで)。大きなブロックは入れない +- `findings` 以外のフィールド (中間状態、エージェントの思考過程など) は返さない +- JSON 以外のテキストを返さない +- severity は項目ごとに knowledge.md / rules.yaml の指示に従い決定する (varies) + +## 誤検知抑制パターン + +以下はヒットしても finding に含めないこと: +- コメント行内 (`// ...`, `# ...`, `/* ... */` 内) +- 文字列リテラル内のサンプルコード (テストデータ等) +- 既に対策済みの設定 (HSTS / Secure クッキー / CSP 設定済み 等) +- ファイル名に `test/`, `spec/`, `__tests__/`, `fixtures/`, `mock/`, `sample/` を含む場合は finding を出すが severity を一段下げる + +## 並列性 + +このサブエージェントは二次サブエージェントを起動しません (Claude Code 仕様)。 +ファイル数が多くてもこのエージェント内では順次処理します。シャーディングはメイン側で済んでいます。 diff --git a/skills/ipa-security-check/commands/ipa-security-check.md b/skills/ipa-security-check/commands/ipa-security-check.md new file mode 100644 index 0000000..c2c41da --- /dev/null +++ b/skills/ipa-security-check/commands/ipa-security-check.md @@ -0,0 +1,71 @@ +--- +name: ipa-security-check +description: IPA 5 資料に基づくソースコード静的セキュリティチェックを実行する +--- + +# /ipa-security-check + +IPA「安全なウェブサイトの作り方 改訂第7版」ほか 4 資料に基づきソースコードを静的検査する。 + +## 使い方 + +``` +/ipa-security-check [] [--diff] [--categories ] [--severity ] [--output ] +``` + +## 引数 + +| 引数 | 必須 | 説明 | +|---|---|---| +| `` | 任意 | スキャン対象パス。ディレクトリまたは glob (例: `src/`, `**/*.php`)。省略時はカレント WD 全体 | +| `--diff` | 任意 | 現ブランチと `main` の差分ファイルのみを対象 | +| `--categories ` | 任意 | カンマ区切りでカテゴリ限定。指定可能値: `sqli, oscmd, traversal, session, xss, csrf, http-header, mail-header, clickjacking, bof, access-control, safe-sql, whc, ops` | +| `--severity ` | 任意 | 報告下限。`critical, high, medium, low, info` のいずれか。デフォルト `low` | +| `--output ` | 任意 | カンマ区切りで出力ファイル指定。デフォルト `ipa-security-report.md,ipa-security-report.sarif` | + +## 使用例 + +``` +# 全体スキャン +/ipa-security-check + +# src/ 配下のみ +/ipa-security-check src/ + +# PHP ファイルのみ +/ipa-security-check '**/*.php' + +# main との差分のみ +/ipa-security-check --diff + +# SQLi と XSS のみ、High 以上 +/ipa-security-check --categories sqli,xss --severity high + +# 出力先を指定 +/ipa-security-check --output report.md,report.sarif src/ +``` + +## 動作 + +1. `SKILL.md` の「実行手順」セクションに従う +2. `lib/scope_resolver.md` で対象ファイル列挙 +3. `lib/shard_planner.md` でカテゴリ別シャーディング +4. `agents/` 配下の検査サブエージェント (14 体) をメインから並列起動。集約 findings は `.tmp/ipa-security-check/findings_raw.json` に保存 +5. **Phase 5**: `scripts/snippet_hash.py` で `snippet_hash` を付与した後、`15-false-positive-review` を **5 件 / shard** で並列起動し、verdicts を `verdicts.json` に集約 +6. **Step 7 出力**: `scripts/render_report.py` を Bash で実行。スクリプト内部で verdict マージ・既存レポートからの triage 引き継ぎ・Markdown + SARIF 生成を一括処理 + +## 出力レポートの 3 セクション + +- `## 検出結果` … 通常 (`未対応` / `対応する` のみ) +- `## 偽陽性候補` … FP レビューで `likely_false_positive` 判定 (本文外) +- `## トリアージ済み (抑止)` … `問題なし` / `保留` (サマリから除外) + +ユーザーは triage ブロックの `status:` を編集することでセクション間を移動できる。 + +## 自然文起動 + +以下のような自然文でもこのコマンドが起動する。 + +- 「IPA のセキュリティチェックをして」 +- 「IPA 安全なウェブサイトの作り方に従ってこのコードを点検して」 +- 「IPA の脆弱性チェックを差分で」 diff --git a/skills/ipa-security-check/knowledge/01_sql_injection.md b/skills/ipa-security-check/knowledge/01_sql_injection.md new file mode 100644 index 0000000..f88dd63 --- /dev/null +++ b/skills/ipa-security-check/knowledge/01_sql_injection.md @@ -0,0 +1,272 @@ +--- +name: sql_injection +ipa_document: 安全なウェブサイトの作り方 改訂第7版 +ipa_section: "1.1 SQLインジェクション" +ipa_page: "6-12" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/sql.html +cwe: CWE-89 +--- + +# SQL インジェクション (SQL Injection) + +## 出典 + +- 文書名: 安全なウェブサイトの作り方 改訂第7版(IPA, 2021年3月31日) +- 章節: 1.1 SQLインジェクション +- ページ: p.6〜12 +- URL: https://www.ipa.go.jp/security/vuln/websecurity/sql.html +- PDF: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017316.pdf +- 詳細は別冊「安全なSQLの呼び出し方」(`safe_sql_details.md` 参照) + +## 概要 + +データベースと連携したウェブアプリケーションにおいて、利用者からの入力情報を基に SQL 文を組み立てる際、その組み立て方法に問題があると発生する脆弱性。 + +**発生原理**: ウェブアプリケーションで直接、文字列連結処理によって SQL 文を組み立てる際、特別な意味を持つ記号文字(`'`、`\`、`;`、`--` など)が適切にエスケープ処理されないまま埋め込まれると、攻撃者による入力値の改変によって SQL 文の意味(構文構造)が変化し、データベースに対して意図しない命令が実行される。 + +CWE-89 に該当。届出受付開始から2014年第4四半期までで、ウェブサイトの届出件数のおよそ 11% を占める、最も頻出する重大脆弱性の一つ。 + +## 脅威・被害 + +| 区分 | 内容 | +|---|---| +| 情報漏えい | データベースに蓄積された非公開情報(個人情報・認証情報等)の閲覧 | +| 改ざん・破壊 | データベース内情報の改ざん・消去(ウェブページ改ざん、パスワード変更、システム停止) | +| 認証回避 | 認証回避による不正ログイン(ログインユーザに許可された全操作が不正実行される) | +| サーバ乗っ取り | ストアドプロシージャ等を経由した OS コマンド実行による、システム乗っ取り・他システム攻撃の踏み台化 | + +## 根本的解決策 + +### 1-(i)-a プレースホルダによる SQL 文組み立て(推奨) + +**SQL 文の組み立ては全てプレースホルダで実装する**。 + +- **静的プレースホルダ(推奨)**: プレースホルダのまま SQL 文をコンパイルし、データベースエンジン側で値を割り当てる方式。ISO/JIS 規格では「準備された文(Prepared Statement)」。原理的に SQL インジェクションの可能性がなくなる。 +- **動的プレースホルダ**: アプリ側の DB 接続ライブラリ内で値をエスケープしてプレースホルダにはめ込む方式。静的より劣る。 + +### 1-(i)-b 文字列連結による SQL 文組み立て時の対策(やむを得ない場合) + +- 文字列型値の埋め込み: シングルクォートで囲み、`'` → `''`、`\` → `\\` 等、DB エンジン提供のリテラル生成専用 API を用いる +- 数値型値の埋め込み: 数値リテラルであることを保証する処理(数値型キャスト等)を実施 +- 外部入力の影響を受ける値だけでなく、**SQL 文を構成するすべてのリテラル生成**で行う +- DB エンジンの種類・設定により処理が異なるため、それぞれに応じた実装が必須 + +### 1-(ii) パラメータへの SQL 文直接指定の禁止 + +hidden 等のパラメータに SQL 文を指定する実装は「論外」(IPA 明記)。 + +## 保険的対策 + +### 1-(iii) エラーメッセージの非表示 + +DB の種類・テーブル構造・実行エラーを起こした SQL 文等を含むエラーメッセージは、攻撃のヒントとなり、UNION-based / Error-based SQLi の結果表示の手段にもなる。ブラウザ上に表示させない。 + +### 1-(iv) データベースアカウントの権限最小化 + +アプリが DB 接続に使うアカウントの権限が必要以上に高いと被害が深刻化する。必要最小限の権限のみ付与する。 + +## NG コードパターン (検出対象) + +### PHP + +```php +// NG: 文字列連結 +$pdo->query("SELECT * FROM users WHERE id = '" . $_GET['id'] . "'"); + +// NG: 変数展開 +$sql = "SELECT * FROM users WHERE name='$name'"; + +// NG: PDO::prepare に文字列連結 +$stmt = $pdo->prepare("SELECT * FROM users WHERE name='" . $name . "'"); + +// NG: addslashes だけで対処 +$name = addslashes($_POST['name']); +$sql = "SELECT * FROM users WHERE name='$name'"; +``` + +### Java + +```java +// NG: Statement + 文字列連結 +Statement st = conn.createStatement(); +st.executeQuery("SELECT * FROM users WHERE id = " + request.getParameter("id")); + +// NG: String.format +String sql = String.format("SELECT * FROM users WHERE id=%s", id); +``` + +### Python + +```python +# NG: f-string / % / .format +cursor.execute(f"SELECT * FROM users WHERE id={user_id}") +cursor.execute("SELECT * FROM users WHERE id=%s" % user_id) +cursor.execute("SELECT * FROM users WHERE id={}".format(user_id)) +``` + +### Ruby / Rails + +```ruby +# NG: 文字列補間 +User.where("id = '#{params[:id]}'") +sql = "SELECT * FROM users WHERE id=" + params[:id] +``` + +### Node.js + +```js +// NG: テンプレートリテラル / + 連結 +db.query(`SELECT * FROM users WHERE id = ${userId}`); +connection.query("SELECT * FROM users WHERE id = " + req.query.id); +``` + +### .NET (C#) + +```csharp +// NG: 文字列連結 / 補間 +var cmd = new SqlCommand("SELECT * FROM Users WHERE Id = '" + id + "'", conn); +string sql = $"SELECT * FROM Users WHERE name='{name}'"; +``` + +### 識別子 (テーブル名・カラム名・ORDER BY) + +```php +// NG: 識別子を直接連結(プレースホルダは識別子には効かない) +$sql = "SELECT * FROM users ORDER BY " . $_GET['sort']; +``` + +### hidden パラメータに SQL 文(IPA 明記の論外パターン) + +```html + +``` + +## OK コードパターン (修正例) + +### PHP (PDO) + +```php +// OK: 静的プレースホルダ +$stmt = $pdo->prepare('SELECT * FROM users WHERE id = :id'); +$stmt->bindValue(':id', $id, PDO::PARAM_INT); +$stmt->execute(); +``` + +### Java (JDBC) + +```java +// OK: PreparedStatement +PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); +ps.setInt(1, id); +ResultSet rs = ps.executeQuery(); +``` + +### Perl (DBI) + +```perl +# OK: bind value +my $sth = $dbh->prepare('SELECT * FROM users WHERE id = ?'); +$sth->execute($id); +``` + +### Ruby (ActiveRecord) + +```ruby +# OK: ハッシュ条件 / 配列形式 +User.where(id: params[:id]) +User.where('id = ?', params[:id]) +``` + +### ASP.NET (ADO.NET) + +```csharp +// OK: 名前付きパラメータ +var cmd = new SqlCommand("SELECT * FROM Users WHERE Id = @id", conn); +cmd.Parameters.AddWithValue("@id", id); +``` + +### Python (DB-API) + +```python +# OK: パラメータ化クエリ +cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) +cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) +``` + +### 識別子のホワイトリスト検証 + +```php +$allowed = ['id', 'name', 'created_at']; +$col = in_array($_GET['sort'], $allowed, true) ? $_GET['sort'] : 'id'; +$sql = "SELECT * FROM users ORDER BY $col"; +``` + +## 自動チェック観点 + +### 静的解析でカバー可能(◎) + +- 文字列連結/文字列補間/テンプレートリテラル/f-string による SQL 構築 +- `mysql_query`, `mysqli_query`, `pg_query`, `Statement#executeQuery`, `cursor.execute(... % ...)` 等のシンクに連結結果を渡している +- `addslashes`, `mysql_real_escape_string`, `htmlspecialchars` のみで対処(プレースホルダ未使用) +- ORM の `where` に raw SQL / 文字列補間 +- hidden に SQL 文をクライアントに渡している +- `Statement.createStatement()` の使用(`PreparedStatement` を使うべき) +- MySQL Connector/J の `characterEncoding=sjis` / `ujis` + `useServerPrepStmts` 未指定(JVN#59748723) + +### 静的解析でカバーしにくい + +- 実行時に変数化されるテーブル名・ORDER BY カラム(taint analysis が必要) +- ストアドプロシージャ内の動的 SQL +- ORM の `raw()` / `execute()` 経由 + +### 検出正規表現候補 + +``` +# 文字列連結による SQL 組み立て (汎用) +(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|VALUES)\b[^;]{0,200}([+.]|\|\|)\s*(\$|params|request|input|user|req\.) + +# PHP: クエリ系関数に文字列連結 +(mysql_query|mysqli_query|mysqli->query|pg_query|->query|->exec)\s*\([^)]*(\.|\${)[^)]*\) + +# Python: cursor.execute with % or .format or f-string +\.execute\(\s*[fF]?["'].*(%s|\{|\+).*["'].*\) + +# Java: Statement#execute* に + 連結 +(executeQuery|executeUpdate|execute)\s*\([^)]*\+[^)]*\) +createStatement\s*\(\s*\) + +# Ruby on Rails: where に文字列補間 +\.where\(\s*["'][^"']*#\{ + +# Node.js: テンプレートリテラルや + で SQL 組立 +(query|execute)\s*\(\s*`[^`]*\$\{ +(query|execute)\s*\([^)]*\+[^)]*\) + +# 危険なエスケープ関数のみへの依存 +(addslashes|mysql_escape_string|mysql_real_escape_string)\s*\( + +# hidden パラメータに SQL 文 +]+type=["']?hidden["']?[^>]+value=["'][^"']*(SELECT|INSERT|UPDATE|DELETE)\b + +# OK signal: Prepared Statement / プレースホルダ +prepare\s*\(|PreparedStatement|bindValue|bindParam|Parameters\.Add|\:\w+ +``` + +## 関連ルール ID + +- IPA-SWS-1-SQLI-001: プレースホルダ未使用の SQL 構築(1-(i)-a 違反) +- IPA-SWS-1-SQLI-002: 文字列連結による SQL(1-(i)-b 違反 / `addslashes` 等のみ依存) +- IPA-SWS-1-SQLI-003: hidden に SQL 文(1-(ii) 違反) +- IPA-SWS-1-SQLI-004: DB エラーメッセージのブラウザ表示(1-(iii) 違反) +- IPA-SWS-1-SQLI-005: DB アカウントの過剰権限(1-(iv) 違反) +- IPA-SWS-1-SQLI-006: 識別子 (ORDER BY 等) への入力直接埋め込み +- IPA-SWS-1-SQLI-007: MySQL Connector/J で characterEncoding=sjis/ujis + 動的プレースホルダ +- IPA-SWS-1-SQLI-008: Perl DBD::mysql で `quote($v, SQL_INTEGER)` の使用 + +## 参考 + +- IPA「安全なウェブサイトの作り方 - 1.1 SQLインジェクション」: https://www.ipa.go.jp/security/vuln/websecurity/sql.html +- IPA「知っていますか?脆弱性 1. SQLインジェクション」: https://www.ipa.go.jp/security/vuln/vuln_contents/sql.html +- CWE-89: https://cwe.mitre.org/data/definitions/89.html +- JVNDB CWE-89: https://jvndb.jvn.jp/ja/cwe/CWE-89.html +- 詳細: 別冊「安全なSQLの呼び出し方」(`safe_sql_details.md` 参照) diff --git a/skills/ipa-security-check/knowledge/02_os_command_injection.md b/skills/ipa-security-check/knowledge/02_os_command_injection.md new file mode 100644 index 0000000..8daf1e1 --- /dev/null +++ b/skills/ipa-security-check/knowledge/02_os_command_injection.md @@ -0,0 +1,244 @@ +--- +name: os_command_injection +ipa_document: 安全なウェブサイトの作り方 改訂第7版 +ipa_section: "1.2 OSコマンド・インジェクション" +ipa_page: "13-15" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/os-command.html +cwe: CWE-78 +--- + +# OS コマンド・インジェクション (OS Command Injection) + +## 出典 + +- 文書名: 安全なウェブサイトの作り方 改訂第7版(IPA, 2021年3月31日) +- 章節: 1.2 OS コマンド・インジェクション +- ページ: p.13〜15 +- URL: https://www.ipa.go.jp/security/vuln/websecurity/os-command.html + +## 概要 + +外部からの攻撃により、ウェブサーバの OS コマンドを不正に実行されてしまう脆弱性。ウェブアプリケーションが外部プログラム(シェルや別プロセス)を呼び出し可能な言語機能を使用する際、ユーザ入力が適切に検証・分離されないと、攻撃者が意図しない OS コマンドを連結・注入して実行できる。 + +CWE-78 に該当。Perl で開発されたウェブアプリケーションや、組み込み製品(無線 LAN ルータ等)の管理画面 CGI プログラムで発見例が多い。 + +## 脅威・被害 + +| 区分 | 内容 | +|---|---| +| ファイル | サーバ内ファイルの閲覧・改ざん・削除、重要情報の漏えい、設定ファイル改ざん | +| システム | OS の不正シャットダウン、ユーザアカウントの追加・変更 | +| マルウェア | 不正プログラムのダウンロード・実行、ウイルス/ワーム/ボットへの感染、バックドア設置 | +| 踏み台化 | サービス不能攻撃、他システムへの攻撃調査、迷惑メール送信 | + +## 発生原因(言語別の危険な関数) + +### Perl +- `open()` … 引数文字列にパイプ `|` を含めると、その後の文字列がシェルコマンドとして実行される +- `system()` +- `eval()` +- バッククォート(`` `...` ``)/qx// + +### PHP +- `exec()`, `passthru()`, `shell_exec()`, `system()`, `popen()`, `proc_open()` +- バッククォート演算子 + +### Ruby +- `system`, `exec`, `` `...` ``, `IO.popen`, `Kernel.open`(パイプ付き), `%x{}` + +### Python +- `os.system`, `os.popen`, `subprocess.*(..., shell=True)`, `commands.getoutput` + +### Java +- `Runtime.exec`, `ProcessBuilder`(シェル経由 `sh -c` を使うパターン) + +### Node.js +- `child_process.exec`, `child_process.execSync` + +### .NET +- `Process.Start`(`UseShellExecute=true` や `cmd.exe /c` を経由するパターン) + +## 根本的解決策 + +### 2-(i) シェルを起動できる言語機能の利用を避ける + +- 可能な限り、シェルを介さない代替関数で代替する +- 例: **Perl では `open()` ではなく `sysopen()` を使う**(IPA が明示)。`sysopen()` はシェルを起動しない +- 外部プログラム呼び出しが必要な場合、シェル展開を行わない「引数配列を直接渡す」インタフェースを用いる(例: `execve` 系、`subprocess.run([...], shell=False)`、`ProcessBuilder(List)`) + +## 保険的対策 + +### 2-(ii) 入力値検証(シェル起動機能を利用せざるを得ない場合) + +「シェルを起動できる言語機能を利用する場合は、その引数を構成する全ての変数に対してチェックを行い、あらかじめ許可した処理のみを実行する」。 + +- **ホワイトリスト方式(推奨)**: 引数に許可する文字の組み合わせを列挙し、それ以外は拒否 + - 数値パラメータなら `^[0-9]+$` のみ許可、等 +- **ブラックリスト方式(非推奨)**: `|`, `<`, `>`, `;`, `&`, `` ` ``, `$`, `(`, `)`, `\n` 等を弾く方式。漏れが生じやすく IPA は非推奨 + +## NG コードパターン (検出対象) + +### Perl + +```perl +# NG: open のパイプによりシェルコマンド実行 +open(my $fh, "/usr/bin/grep $keyword file.txt|") or die; +``` + +### PHP + +```php +// NG: 文字列連結でシェルへ渡す +system("convert " . $_GET['file'] . " out.png"); +$out = shell_exec("ls " . $_GET['dir']); +exec("ping " . $_GET['host'], $output); +// NG: escapeshellarg / escapeshellcmd は保険、根本対策ではない +exec('ping ' . escapeshellarg($host), $output); +``` + +### Python + +```python +# NG: shell=True かつ文字列連結 +subprocess.call("ls " + user_input, shell=True) +os.system("convert " + filename + " out.png") +``` + +### Java + +```java +// NG: sh -c に文字列を渡す +Runtime.getRuntime().exec("sh -c \"ls " + userInput + "\""); +``` + +### Ruby + +```ruby +# NG: 文字列補間でシェルに渡す +system("ls #{params[:dir]}") +`ls #{params[:dir]}` +IO.popen("ls #{params[:dir]}") +``` + +### Node.js + +```js +// NG: child_process.exec への文字列連結 +child_process.exec("ls " + userInput); +``` + +## OK コードパターン (修正例) + +### Perl + +```perl +# OK: sysopen はシェルを起動しない +sysopen(my $fh, $filename, O_RDONLY) or die; + +# OK: list-form は exec 直渡しでシェルを介さない +open(my $fh, '-|', '/usr/bin/grep', $keyword, 'file.txt'); +system('/usr/bin/grep', $keyword, 'file.txt'); +``` + +### Python + +```python +# OK: 引数を list で渡し shell=False (デフォルト) +subprocess.run(["convert", filename, "out.png"], shell=False, check=True) +``` + +### Java + +```java +// OK: ProcessBuilder + 引数配列 +ProcessBuilder pb = new ProcessBuilder("ls", userInput); +pb.start(); +``` + +### Ruby + +```ruby +# OK: 引数を分離 +system("ls", params[:dir]) +IO.popen(["ls", params[:dir]]) +``` + +### Node.js + +```js +// OK +child_process.execFile("ls", [userInput]); +child_process.spawn("ls", [userInput]); +``` + +### PHP + +```php +// OK: 可能なら言語標準 API で代替(シェル不使用) +$contents = file_get_contents($safe_path); +``` + +## 自動チェック観点 + +### 静的解析でカバー可能(◎) + +- シェル起動関数(`system`, `exec`, `shell_exec`, `popen`, `passthru` 等)に文字列連結/テンプレートリテラル/文字列補間で外部入力を含めている +- Perl `open` の引数に `|` を含む文字列、または末尾 `|` がある +- `subprocess.*` / `child_process.exec` / `Runtime.exec` で 1 個の文字列にコマンド全体を渡している +- Python で `shell=True` が指定されている +- `escapeshellarg`/`escapeshellcmd` のみで対処(要レビュー) + +### 静的解析でカバーしにくい + +- 内部 API 経由のコマンド実行 +- レスポンス本文に何も返さないコマンド実行 + +### 検出正規表現候補 + +``` +# Perl: open に | 付き、または文字列補間 +\bopen\s*\(\s*[^,]+,\s*["'][^"']*\$\w+[^"']*\|["'] +\bopen\s*\(\s*[^,]+,\s*["'][^"']*\|\s*\$\w+ + +# Perl/PHP/Ruby: バッククォート + 変数 +`[^`]*\$[A-Za-z_][^`]*` + +# PHP: 危険関数 + 連結 +\b(system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec)\s*\([^)]*(\$_(GET|POST|REQUEST|COOKIE|SERVER)|\.\s*\$) + +# Python: shell=True +subprocess\.(call|run|Popen|check_call|check_output)\s*\([^)]*shell\s*=\s*True +\bos\.(system|popen)\s*\( + +# Java: Runtime.exec 文字列1個 +Runtime\.getRuntime\(\)\.exec\(\s*["'][^"']*\+ + +# Ruby +\bsystem\s*\(\s*["'][^"']*#\{ +\bIO\.popen\s*\(\s*["'][^"']*#\{ + +# Node.js +child_process\.(exec|execSync)\s*\(\s*[`"'][^`"']*(\$\{|\+) + +# .NET +new\s+Process\s*\(\s*\)[^;]*UseShellExecute\s*=\s*true +Process\.Start\s*\(\s*["'][^"']*\+ + +# 保険的: escapeshellarg / escapeshellcmd 単独使用 (要レビュー) +\bescapeshell(arg|cmd)\s*\( +``` + +## 関連ルール ID + +- IPA-SWS-2-OSI-001: シェル起動関数への文字列連結(2-(i) 違反) +- IPA-SWS-2-OSI-002: Perl `open` でパイプ付き引数(2-(i) 違反) +- IPA-SWS-2-OSI-003: Python `shell=True`(2-(i) 違反) +- IPA-SWS-2-OSI-004: Java `Runtime.exec` に文字列1個(2-(i) 違反) +- IPA-SWS-2-OSI-005: 入力ホワイトリスト検証なし(2-(ii) 違反) +- IPA-SWS-2-OSI-006: `escapeshellarg`/`escapeshellcmd` のみ依存 + +## 参考 + +- IPA「安全なウェブサイトの作り方 - 1.2 OSコマンド・インジェクション」: https://www.ipa.go.jp/security/vuln/websecurity/os-command.html +- CWE-78: https://cwe.mitre.org/data/definitions/78.html +- 届出事例: 複数のASUS製無線LANルータ / 「Usermin」 / 「Movable Type」 diff --git a/skills/ipa-security-check/knowledge/03_directory_traversal.md b/skills/ipa-security-check/knowledge/03_directory_traversal.md new file mode 100644 index 0000000..0eaa6ab --- /dev/null +++ b/skills/ipa-security-check/knowledge/03_directory_traversal.md @@ -0,0 +1,242 @@ +--- +name: directory_traversal +ipa_document: 安全なウェブサイトの作り方 改訂第7版 +ipa_section: "1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル" +ipa_page: "16-18" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/parameter.html +cwe: CWE-22 +--- + +# パス名パラメータの未チェック/ディレクトリ・トラバーサル (Path Traversal) + +## 出典 + +- 文書名: 安全なウェブサイトの作り方 改訂第7版(IPA, 2021年3月31日) +- 章節: 1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル +- ページ: p.16〜18 +- URL: https://www.ipa.go.jp/security/vuln/websecurity/parameter.html + +## 概要 + +ウェブアプリケーションが外部からのパラメータでウェブサーバ内のファイル名を直接指定する実装に問題がある場合に発生する脆弱性。攻撃者がパラメータを改変して `../` や絶対パスを送り込むことで、本来公開すべきでないファイルを閲覧・改ざん・削除できてしまう。 + +CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) に該当。 + +### 脆弱性が生じやすい場面 + +- ウェブページのテンプレートをファイルから読み込む処理 +- HTML の hidden パラメータでサーバ内ファイル名を直接指定する設計 +- ユーザ入力を指定ファイルへ直接書き込む処理 + +## 脅威・被害 + +| 区分 | 内容 | +|---|---| +| 情報漏えい | サーバ内ファイルの閲覧(個人情報・認証情報・ソースコード・設定ファイル等の漏えい) | +| 改ざん・破壊 | 設定ファイル・データファイル・プログラムソースコードの改ざん・削除 | + +## 根本的解決策 + +### 3-(i)-a 外部パラメータでサーバ内ファイル名を直接指定する実装を避ける + +仕様設計段階で、外部パラメータがファイル名そのものを示す設計を避ける。ファイル本体は ID(連番・UUID 等)で参照させ、実体パスはサーバ側で固定的にマッピングする。 + +### 3-(i)-b 固定ディレクトリ + basename 化 + +``` +open(dirname + basename(filename)) +``` + +1. あらかじめ固定ディレクトリ `dirname` を決め打ち +2. `basename()` 等の API を使い、入力からファイル名部分のみを抽出(パス区切り `/`, `\` を除去) +3. 結合したパスでファイルを開く + +これにより、絶対パス指定と `../` トラバーサルの両方を回避できる。 + +## 保険的対策 + +### 3-(ii) アクセス権限の適切な管理 + +ウェブサーバプロセスがアクセスできるファイルを最小限に制限し、OS/ファイルシステムのパーミッションで意図しないファイル参照を拒否する。 + +### 3-(iii) ファイル名のチェック + +入力パラメータから次のいずれかを検出したら処理を中止する: + +- `/` +- `../` +- `..\` +- OS 固有のパス解釈でディレクトリ移動可能な文字列 + +#### URL エンコード・二重エンコード対策 + +- 1 重エンコード: `%2F` (`/`), `..%2F`, `..%5C` +- 2 重エンコード: `%252F`, `..%252F`, `..%255C` +- NULL バイト: `%00` (拡張子チェックバイパスに悪用) + +## NG コードパターン (検出対象) + +### PHP + +```php +// NG: 入力直渡し +$content = file_get_contents($_GET['file']); +include($_GET['template']); +readfile($_REQUEST['file']); +``` + +### Java + +```java +// NG +File f = new File(request.getParameter("file")); +FileInputStream fis = new FileInputStream(f); +``` + +### Python + +```python +# NG +open(request.args.get('path')) +send_file(request.args.get('filename')) +``` + +### Ruby + +```ruby +# NG +File.read(params[:file]) +File.open(params[:name]) +``` + +### Node.js + +```js +// NG +fs.readFile(req.query.file, ...); +res.sendFile(userInput); +``` + +### .NET (C#) + +```csharp +// NG +var content = File.ReadAllText(Request.QueryString["file"]); +``` + +## OK コードパターン (修正例) + +### PHP + +```php +$dir = '/var/www/templates/'; +$file = basename($_GET['file']); // パス区切りを除去 +$path = realpath($dir . $file); // 正規化 +if ($path !== false && strpos($path, $dir) === 0) { + $content = file_get_contents($path); +} +``` + +### Java + +```java +Path base = Paths.get("/var/www/templates").toRealPath(); +Path target = base.resolve(name).normalize().toRealPath(); +if (!target.startsWith(base)) { + throw new SecurityException(); +} +``` + +### Perl + +```perl +use File::Basename; +my $name = basename($input); +my $path = "/var/www/templates/$name"; +open(my $fh, '<', $path) or die; +``` + +### C# (.NET) + +```csharp +var baseDir = Path.GetFullPath(@"C:\app\templates\"); +var name = Path.GetFileName(Request.QueryString["file"]); +var path = Path.GetFullPath(Path.Combine(baseDir, name)); +if (!path.StartsWith(baseDir)) throw new SecurityException(); +``` + +### Ruby + +```ruby +base = '/var/www/templates' +name = File.basename(params[:file]) +path = File.expand_path(File.join(base, name)) +raise unless path.start_with?(base + '/') +File.read(path) +``` + +## 自動チェック観点 + +### 静的解析でカバー可能(◎) + +- 外部入力が、ファイル open 系 API の引数(特に第1引数)に直接渡されている +- `include`, `require` (PHP) の引数に外部入力が混入 +- `basename()` / `Path.GetFileName` / `File.basename` を経由していない +- ベースディレクトリと正規化済みパスの prefix 一致確認がない +- ファイル名チェックが「`../` だけ」しか見ていない(`..%2F`, `..%252F`, NUL バイト, `\` を考慮していない) + +### 静的解析でカバーしにくい + +- 内部 ID → ファイル名マッピングの整合性 +- マッピングテーブルへの SQLi 経由の迂回 + +### 検出正規表現候補 + +``` +# PHP: 入力直渡しのファイル系 API +\b(fopen|file_get_contents|file|readfile|file_put_contents|copy|unlink|include|include_once|require|require_once|SplFileObject)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE|SERVER)\[ + +# Java +new\s+File\s*\(\s*request\.getParameter\( +new\s+FileInputStream\s*\(\s*request\.getParameter\( +\bPaths\.get\s*\(\s*request\.getParameter\( + +# Python +\bopen\s*\(\s*(request\.|flask\.request\.|os\.environ|sys\.argv) +\b(send_file|send_from_directory)\s*\([^)]*request\. + +# Ruby / Rails +\bFile\.(read|open|new|delete)\s*\(\s*params\[ +\bsend_file\s*\(\s*params\[ + +# Node.js +\bfs\.(readFile|readFileSync|createReadStream|writeFile|writeFileSync|unlink)\s*\(\s*(req\.(query|body|params)|request\.) + +# .NET +File\.(ReadAllText|ReadAllBytes|Open|OpenRead|WriteAllText|Delete)\s*\(\s*Request\. + +# パスインジェクション系の入力検出 +(\.\./|\.\.\\|%2e%2e%2f|%2e%2e/|\.\.%2f|\.\.%5c|%252e%252e%252f|%c0%ae%c0%ae/) + +# OK signal: basename 系 API 使用 +\b(basename|Path\.GetFileName|File\.basename|os\.path\.basename|path\.basename)\s*\( + +# OK signal: 正規化 + prefix 確認 +realpath\s*\(|toRealPath\s*\(|Path\.GetFullPath\s*\(|File\.expand_path\s*\( +(startsWith|StartsWith|start_with\?|strpos)\s*\(.*base +``` + +## 関連ルール ID + +- IPA-SWS-3-PATH-001: ファイル系 API への入力直渡し(3-(i)-a 違反) +- IPA-SWS-3-PATH-002: basename / Path.GetFileName 等の未使用(3-(i)-b 違反) +- IPA-SWS-3-PATH-003: ベースディレクトリ prefix 確認なし +- IPA-SWS-3-PATH-004: `../` のみのチェック(エンコード/NULL バイト未対応 / 3-(iii) 不完全) +- IPA-SWS-3-PATH-005: `include`/`require` に外部入力(LFI 候補) + +## 参考 + +- IPA「安全なウェブサイトの作り方 - 1.3 ディレクトリ・トラバーサル」: https://www.ipa.go.jp/security/vuln/websecurity/parameter.html +- CWE-22: https://cwe.mitre.org/data/definitions/22.html +- JVNDB CWE-22: https://jvndb.jvn.jp/ja/cwe/CWE-22.html +- 届出事例: JVNDB-2015-000006 シンクグラフィカ「ダウンロードログ CGI」 / JVNDB-2014-000054 Spring Framework / JVNDB-2013-000084 VMware ESX / ESXi diff --git a/skills/ipa-security-check/knowledge/04_session_management.md b/skills/ipa-security-check/knowledge/04_session_management.md new file mode 100644 index 0000000..94e2fc1 --- /dev/null +++ b/skills/ipa-security-check/knowledge/04_session_management.md @@ -0,0 +1,292 @@ +--- +name: session_management +ipa_document: 安全なウェブサイトの作り方 改訂第7版 +ipa_section: "1.4 セッション管理の不備" +ipa_page: "19-25" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/session-management.html +cwe: CWE-330 / CWE-384 / CWE-522 / CWE-614 +--- + +# セッション管理の不備 (Session Management Flaws / Session Fixation) + +## 出典 + +- 文書名: 安全なウェブサイトの作り方 改訂第7版(IPA, 2021年3月31日) +- 章節: 1.4 セッション管理の不備 +- ページ: p.19〜25 +- URL: https://www.ipa.go.jp/security/vuln/websecurity/session-management.html + +## 概要 + +ウェブアプリケーションにおいて、利用者を識別するセッション ID の発行・管理に不備がある場合、攻撃者がセッション ID を不正に取得し、利用者になりすまして無認可アクセスを行える脆弱性。 + +主な攻撃手法: + +- **セッション ID 推測攻撃**: 時刻情報など単純なアルゴリズムで生成されたセッション ID が予測可能 +- **セッション ID 盗用攻撃**: Referer 送信機能による URL 露出、通信盗聴、XSS、ネットワーク傍受 +- **セッション ID 固定化攻撃(Session Fixation)**: 攻撃者が用意したセッション ID を利用者に送り込む + +## 脅威・被害 + +攻撃が成功すると、攻撃者は利用者になりすまし、その利用者本人に許可されている操作を不正に行うことが可能になる。 + +- **不正な金銭処理**: 不正な送金、利用者が意図しない商品購入、退会処理 等 +- **情報の改ざん・登録**: 各種設定の不正な変更、掲示板への不適切な書き込み 等 +- **非公開情報の不正閲覧**: 個人情報・ウェブメール・会員専用掲示板の不正閲覧 等 + +### 特に注意が必要なウェブサイト + +金銭処理サイト(ネットバンキング、ショッピング、オークション)、非公開情報サイト、ログイン機能を持つサイト全般。 + +## 根本的解決策 + +### 4-(i) セッション ID を推測困難なものにする + +セッション ID は生成アルゴリズムに **暗号論的擬似乱数生成器(CSPRNG)** を用いるなどして、予測困難なものにする。可能な限り自前実装は避け、**ウェブアプリケーションサーバ製品の既存セッション管理機能を利用**することが推奨される。 + +### 4-(ii) セッション ID を URL パラメータに格納しない + +セッション ID は **Cookie に格納**するか、POST メソッドの **hidden パラメータ**に格納して受け渡す。 + +URL Rewriting(Cookie 拒否時の自動切り替え機能)は無効化することを検討する。 + +### 4-(iii) HTTPS 通信で利用する Cookie には secure 属性を加える + +HTTPS 通信で利用する Cookie には `Secure` 属性を必ず付与する。HTTP 通信で Cookie を利用する場合は、HTTPS で発行する Cookie とは別のものを発行する。あわせて、`HttpOnly` 属性、`SameSite` 属性(`Lax`/`Strict`)も付与することが望ましい。 + +### 4-(iv)-a ログイン成功後に、新しいセッションを開始する + +ログイン成功した時点から新しいセッションを開始する(新しいセッション ID でセッション管理する)。新しいセッションを開始する際には、既存のセッション ID を無効化する。これによりセッション固定化攻撃を防止する。 + +### 4-(iv)-b ログイン成功後に、既存セッション ID とは別の秘密情報を発行 + +セッション ID とは別に、ログイン成功時に秘密情報を作成して Cookie にセットし、全てのページでこの秘密情報と Cookie の値が一致することを確認する。秘密情報の生成には暗号処理(CSPRNG)を用いる。 + +4-(iv)-a を採用している場合、または「セッション ID をログイン前には発行せず、ログイン成功後に発行する」実装の場合は、4-(iv)-b は不要。 + +## 保険的対策 + +### 4-(v) セッション ID を固定値にしない + +利用者ごとに固定の値ではなく、ログインごとに新しく発行する(4-(iv)-a で実現される)。 + +### 4-(vi) セッション ID を Cookie にセットする場合、有効期限の設定に注意する + +Cookie の有効期限を短く設定し、必要以上の期間ブラウザに残らないようにする。サーバ側のセッションタイムアウトも併用する。 + +### その他 + +- ログアウト機能を提供し、ログアウト時にサーバ側のセッションを破棄する +- 一定時間操作がない場合の自動セッションタイムアウト +- 重要操作前の再認証(パスワード再入力) + +## NG コードパターン (検出対象) + +### PHP + +```php +// NG: 時刻ベースのセッション ID を自前で生成 +$sid = md5($username . time()); +setcookie('sid', $sid); + +// NG: URL パラメータでセッション ID を引き回す +echo 'マイページ'; + +// NG: ログイン成功後にセッション ID を再生成していない +session_start(); +if (authenticate($u, $p)) { + $_SESSION['user_id'] = $u; // session_regenerate_id() を呼んでいない +} + +// NG: Secure/HttpOnly/SameSite なしの Cookie 発行 +setcookie('sid', $sid); +``` + +| 観点 | 危険コード例 | +|------|-------------| +| 自前のセッション ID 生成 | `md5(time())`, `sha1(microtime())`, `uniqid()` をセッション ID として使用 | +| URL Rewriting 利用 | `session.use_trans_sid=1`、URL に `;jsessionid=...`、`?sid=...` を出力 | +| ログイン後の ID 再生成なし | `authenticate(...)` 成功直後に `session_regenerate_id` / `invalidate()` / `Session.Abandon()` が呼ばれていない | +| Secure 属性なし | `setcookie('sid', $sid)`(Secure 指定なし) | +| HttpOnly 属性なし | `setcookie(...)` で `httponly` キーなし | +| SameSite 属性なし | Cookie 発行で `samesite` 未指定 | +| 長すぎる有効期限 | `setcookie(..., time()+60*60*24*365, ...)` のような1年超 | +| ログアウト未実装 | `session_destroy()` / `invalidate()` が呼ばれない | + +## OK コードパターン (修正例) + +### PHP + +```php +// セッション Cookie 属性を ini 設定で固定(推奨) +ini_set('session.cookie_httponly', 1); +ini_set('session.cookie_secure', 1); +ini_set('session.cookie_samesite', 'Strict'); +ini_set('session.cookie_lifetime', 1800); +ini_set('session.use_only_cookies', 1); +ini_set('session.use_trans_sid', 0); + +session_start(); + +if (authenticate_user($username, $password)) { + session_regenerate_id(true); // ログイン成功時に必ず再生成 + $_SESSION['user_id'] = $user_id; + $_SESSION['authenticated'] = true; +} + +setcookie('sessionid', $session_id, [ + 'secure' => true, + 'httponly' => true, + 'samesite' => 'Strict', + 'expires' => time() + 1800, + 'path' => '/', +]); +``` + +### Java (Servlet) + +```java +if (authenticateUser(username, password)) { + HttpSession existing = request.getSession(false); + if (existing != null) existing.invalidate(); + + HttpSession newSession = request.getSession(true); + newSession.setMaxInactiveInterval(30 * 60); + + response.addHeader("Set-Cookie", + "JSESSIONID=" + newSession.getId() + + "; Path=/; Secure; HttpOnly; SameSite=Strict"); +} +``` + +```xml + + + + true + true + + COOKIE + +``` + +### ASP.NET + +```csharp +if (AuthenticateUser(username, password)) +{ + Session.Abandon(); + Session.Clear(); + + var authCookie = new HttpCookie("...") + { + Secure = true, + HttpOnly = true, + SameSite = SameSiteMode.Strict, + Expires = DateTime.Now.AddMinutes(30), + }; + Response.Cookies.Add(authCookie); +} +``` + +### Express (Node.js) + +```js +app.use(session({ + name: 'sid', + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + secure: true, + httpOnly: true, + sameSite: 'strict', + maxAge: 1000 * 60 * 30, + }, +})); + +req.session.regenerate(err => { + req.session.userId = user.id; +}); +``` + +## 自動チェック観点 + +### 静的解析でカバー可能(○) + +- 自作の弱い乱数によるセッション ID 生成 (`md5(time())`, `uniqid()` 等) +- Cookie 発行で `Secure`/`HttpOnly`/`SameSite` 属性の欠落 +- `session.use_trans_sid=1`/`session.use_only_cookies=0` の設定 +- `session_regenerate_id` 等の再生成 API の不存在(フォルダ全体) +- ログアウト時に `session_destroy()`/`invalidate()` がない + +### 静的解析でカバーしにくい + +- セッション ID のエントロピー実測値 +- ログイン成功後の状態遷移を伴うセッション再生成検証 +- セッションタイムアウト値の妥当性判定 + +### 検出正規表現候補 + +```bash +# 1. セッション ID 自前生成(弱い乱数) +grep -rEn "md5\s*\(\s*(time\(\)|microtime|uniqid|date\()" . +grep -rEn "sha1\s*\(\s*(time\(\)|microtime|uniqid|date\()" . +grep -rEn "uniqid\s*\(" . | grep -iE "session|sid|token" + +# 2. URL にセッション ID 露出 +grep -rEn "\?(sid|sessionid|PHPSESSID|jsessionid)=" . +grep -rEn "session\.use_trans_sid\s*=\s*1" . + +# 3. Cookie 属性不足 +grep -rEn "setcookie\s*\(" . | grep -v -iE "secure|httponly|samesite" + +# 4. ログイン後の再発行漏れ(要文脈確認) +grep -rEn "session_regenerate_id" . + +# 5. ログアウト処理不足 +grep -rEn "logout|signout|sign_out" . | grep -v -iE "session_destroy|invalidate|abandon" + +# 6. PHP ini / web.config / web.xml の不備 +grep -rEn "session\.cookie_secure" . +grep -rEn "session\.cookie_httponly" . +grep -rEn "session\.cookie_samesite" . +grep -rEn "httpCookies[^>]*requireSSL=\"false\"" . +grep -rEn "\s*false\s*" . + +# 7. 長すぎる Cookie 有効期限(1年超など) +grep -rEn "time\(\)\s*\+\s*[0-9]{7,}" . +``` + +## 関連ルール ID + +- IPA-SWS-4-SESS-001: 弱い乱数によるセッション ID 生成(4-(i) 違反) +- IPA-SWS-4-SESS-002: URL Rewriting でセッション ID 露出(4-(ii) 違反) +- IPA-SWS-4-SESS-003: Cookie の Secure 属性なし(4-(iii) 違反) +- IPA-SWS-4-SESS-004: Cookie の HttpOnly 属性なし +- IPA-SWS-4-SESS-005: Cookie の SameSite 属性なし +- IPA-SWS-4-SESS-006: ログイン後のセッション ID 再生成なし(4-(iv)-a 違反) +- IPA-SWS-4-SESS-007: 第2セッション ID/秘密情報の検証なし(4-(iv)-b 違反) +- IPA-SWS-4-SESS-008: セッション ID が固定値(4-(v) 違反) +- IPA-SWS-4-SESS-009: Cookie の有効期限が過剰に長い(4-(vi) 違反) +- IPA-SWS-4-SESS-010: ログアウト時のセッション破棄なし + +## Skill チェックリスト + +- [ ] セッション ID はフレームワーク標準(あるいは CSPRNG)で生成されているか +- [ ] セッション ID は Cookie のみで管理されているか(URL Rewriting 無効) +- [ ] ログイン成功直後にセッション ID が再発行されているか +- [ ] Cookie に `Secure`、`HttpOnly`、`SameSite` 属性が付与されているか +- [ ] HTTPS サイトで `Secure` 属性なしの Cookie が発行されていないか +- [ ] セッションタイムアウト(サーバ側 / Cookie 有効期限)が適切に設定されているか +- [ ] ログアウト処理でサーバ側セッションが確実に破棄されているか +- [ ] 重要操作前にパスワード再認証が行われているか + +## 参考 + +- IPA「安全なウェブサイトの作り方 - 1.4 セッション管理の不備」: https://www.ipa.go.jp/security/vuln/websecurity/session-management.html +- 「CSRF」と「Session Fixation」の諸問題について(高木浩光, IPA): https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000197by-att/20060228_3.pdf +- CWE-330 (Insufficiently Random Values): https://cwe.mitre.org/data/definitions/330.html +- CWE-384 (Session Fixation): https://cwe.mitre.org/data/definitions/384.html +- CWE-522 (Insufficiently Protected Credentials): https://cwe.mitre.org/data/definitions/522.html +- CWE-614 (Sensitive Cookie in HTTPS Without 'Secure'): https://cwe.mitre.org/data/definitions/614.html diff --git a/skills/ipa-security-check/knowledge/05_xss.md b/skills/ipa-security-check/knowledge/05_xss.md new file mode 100644 index 0000000..38b4106 --- /dev/null +++ b/skills/ipa-security-check/knowledge/05_xss.md @@ -0,0 +1,316 @@ +--- +name: xss +ipa_document: 安全なウェブサイトの作り方 改訂第7版 +ipa_section: "1.5 クロスサイト・スクリプティング" +ipa_page: "26-32" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html +cwe: CWE-79 +--- + +# クロスサイト・スクリプティング (Cross-Site Scripting / XSS) + +## 出典 + +- 文書名: 安全なウェブサイトの作り方 改訂第7版(IPA, 2021年3月31日) +- 章節: 1.5 クロスサイト・スクリプティング +- ページ: p.26〜32 +- URL: https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html + +## 概要 + +ウェブアプリケーションが、検索のキーワード表示画面、個人情報登録時の確認画面、掲示板、ウェブログ統計画面等、利用者からの入力内容や HTTP ヘッダの情報を処理し、ウェブページとして出力する際、ウェブページへの出力処理に問題がある場合、そのウェブページにスクリプト等を埋め込まれてしまう脆弱性。 + +XSS の影響は、ウェブサイト自体に対してではなく、そのウェブサイトのページを閲覧している利用者に及ぶ。 + +XSS は出力先(コンテキスト)によって以下のように分類される(自動チェックの観点で重要)。 + +- **HTML 要素内容(body 内テキスト)への出力** +- **HTML 属性値への出力** +- **URL 属性値(`href`, `src`, `action` 等)への出力** +- **JavaScript(`` 要素の内容を動的に生成しない + +- 外部入力に依存して `"); +``` + +```jsp + +``` + +### Perl +```perl +use HTML::Entities; +print encode_entities($value, q{<>&"'}); +``` + +### ASP.NET +```csharp +@Html.Encode(model.Value) +HttpUtility.HtmlEncode(value) +HttpUtility.JavaScriptStringEncode(value) +``` + +### Ruby on Rails +```erb +<%= user_input %> <%# ERB は自動エスケープ %> +<%= sanitize html_text %> <%# 許可タグのみ %> +``` + +### Node.js (Express) +```js +const escape = require('escape-html'); +res.send(escape(value)); + +const u = new URL(input); +if (!['http:', 'https:'].includes(u.protocol)) throw new Error('invalid scheme'); +``` + +### React / Vue +```jsx +// React: {value} は自動エスケープ +
{value}
+``` + +### レスポンスヘッダ +``` +Content-Type: text/html; charset=UTF-8 +Set-Cookie: name=value; Secure; HttpOnly; SameSite=Lax +Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self' +X-XSS-Protection: 1; mode=block +``` + +## 自動チェック観点 + +### 静的解析でカバー可能(◎) + +- テンプレートの生出力タグ +- `innerHTML`/`document.write`/`eval` 系(DOM Based XSS) +- React の `dangerouslySetInnerHTML`、Vue の `v-html`、Angular の `[innerHTML]`/`bypassSecurityTrust*` +- `` 要素の内容を動的に生成しない | スクリプトタグ内への変数埋込検出 | +| 5-(iv) | 根本 | スタイルシートを任意のサイトから取り込めるようにしない | CSS インクルードの動的化禁止 | +| 5-(v) | 保険 | 入力値の内容チェックを行う | 入力バリデーションの有無 | + +### HTML テキストの入力を許可する場合 + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 5-(vi) | 根本 | 入力された HTML テキストから構文解析木を作成し、スクリプトを含まない必要な要素のみを抽出する | DOMPurify / sanitize-html / Bleach 等の利用 | +| 5-(vii) | 保険 | 入力された HTML テキストから、スクリプトに該当する文字列を排除する | 正規表現除去はアンチパターンとして警告 | + +### 全アプリ共通 + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 5-(viii) | 根本 | HTTP レスポンスヘッダの Content-Type フィールドに文字コードを指定 | `Content-Type: text/html; charset=utf-8` の付与 | +| 5-(ix) | 保険 | Cookie 漏えい対策として HttpOnly 属性を加え、TRACE メソッドを無効化 | HttpOnly 属性確認、TRACE メソッド遮断 | +| 5-(x) | 保険 | ブラウザ機能を有効化するレスポンスヘッダを返す | CSP, X-Content-Type-Options, X-XSS-Protection 等 | + +詳細は `05_xss.md` 参照。 + +--- + +## 6. CSRF + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 6-(i)-a | 根本 ※ | POST メソッド + hidden パラメータに秘密情報を挿入し、実行ページで値が正しい場合のみ処理 | CSRF トークンの hidden フィールド検査 | +| 6-(i)-b | 根本 ※ | 処理を実行する直前のページで再度パスワードの入力を求める | 重要操作前の再認証実装 | +| 6-(i)-c | 根本 ※ | Referer が正しいリンク元かを確認し、正しい場合のみ処理 | Referer チェックの有無 | +| 6-(ii) | 保険 | 重要な操作を行った際に、その旨を登録済みメールアドレスに自動送信 | 通知メール実装 | + +詳細は `06_csrf.md` 参照。 + +--- + +## 7. HTTP ヘッダ・インジェクション + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 7-(i)-a | 根本 ※ | ヘッダの出力を直接行わず、ヘッダ出力用 API を使用する | 生のヘッダ出力検出 | +| 7-(i)-b | 根本 ※ | API を利用できない場合は、改行を許可しないよう開発者自身で適切な処理を実装 | 改行除去処理の検査 | +| 7-(ii) | 保険 | 外部からの入力すべてについて、改行コードを削除する | 入力サニタイズ | + +詳細は `07_http_header_injection.md` 参照。 + +--- + +## 8. メールヘッダ・インジェクション + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 8-(i)-a | 根本 ※ | メールヘッダを固定値にして、外部入力はすべてメール本文に出力する | 動的ヘッダ生成の検出 | +| 8-(i)-b | 根本 ※ | メール送信用 API を使用する | 安全な送信ライブラリ利用 | +| 8-(ii) | 根本 | HTML で宛先を指定しない | フォーム hidden での宛先指定検出 | +| 8-(iii) | 保険 | 外部からの入力すべてについて、改行コードを削除する | 入力サニタイズ | + +詳細は `08_mail_header_injection.md` 参照。 + +--- + +## 9. クリックジャッキング + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 9-(i)-a | 根本 ※ | HTTP レスポンスヘッダに X-Frame-Options を出力し、他ドメインからの frame/iframe を制限 | `X-Frame-Options` ヘッダ付与 | +| 9-(i)-b | 根本 ※ | 処理実行直前のページで再度パスワード入力を求める | 重要操作前の再認証 | +| 9-(ii) | 保険 | 重要な処理は一連の操作をマウスのみで実行できないようにする | 確認モーダルやキーボード必須化 | + +詳細は `09_clickjacking.md` 参照。 + +--- + +## 10. バッファオーバーフロー + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 10-(i)-a | 根本 ※ | 直接メモリにアクセスできない言語で記述する | 言語選択(C/C++ ネイティブ部分の存在検出) | +| 10-(i)-b | 根本 ※ | 直接メモリにアクセスできる言語で記述する部分を最小限にする | FFI / native binding の検出 | +| 10-(ii) | 根本 | 脆弱性が修正されたバージョンのライブラリを使用する | 既知 CVE のあるネイティブ依存検出 | + +詳細は `10_buffer_overflow.md` 参照。 + +--- + +## 11. アクセス制御や認可制御の欠落 + +| 項目 ID | 性質 | 実施項目 | 自動チェック観点 | +| --- | --- | --- | --- | +| 11-(i) | 根本 | アクセス制御機能による防御措置が必要なサイトには、パスワード等の秘密情報の入力を必要とする認証機能を設ける | 認証要否レビュー | +| 11-(ii) | 根本 | 認証機能に加えて認可制御の処理を実装し、ログイン中の利用者が他人になりすましてアクセスできないようにする | 認可チェックの有無 | + +詳細は `11_access_control.md` 参照。 + +--- + +# Part 2: 安全なウェブサイトの運用管理に向けての20ヶ条 + +URL: https://www.ipa.go.jp/security/vuln/websecurity/sitecheck.html + +## 大分類 + +| カテゴリ | 該当条文 | +| --- | --- | +| 1. ウェブアプリケーションのセキュリティ対策 | 第1〜第8条 | +| 2. ウェブサーバのセキュリティ対策 | 第9〜第14条 | +| 3. ネットワークのセキュリティ対策 | 第15〜第18条 | +| 4. その他のセキュリティ対策 | 第19〜第20条 | + +--- + +## A. ウェブアプリケーションのセキュリティ対策(第1〜第8条) + +### 第1条: 公開すべきでないファイルを公開していない + +- **推奨対策**: 設定ファイル/秘密情報を公開ディレクトリ外に保管、不要ファイルの削除 +- **自動チェック観点**: + - 公開ディレクトリ(`public/`, `static/`, `www/`, `htdocs/`, `wwwroot/`, `docroot/`)配下に存在しないか: + - 機密設定ファイル: `.env`, `.env.local`, `.env.production`, `config.php`, `application.yml`, `web.config`, `database.yml`, `secrets.json`, `credentials.json`, `firebase-adminsdk*.json` + - VCS ディレクトリ: `.git/`, `.svn/`, `.hg/`, `.DS_Store` + - バックアップ/一時ファイル: `*.bak`, `*.swp`, `*.orig`, `*.old`, `*~` + - ダンプ/ログ: `*.sql`, `*.dump`, `*.log` + - IDE 設定: `.idea/`, `.vscode/`, `*.iml` + - シークレット直書き: `AWS_SECRET`, `API_KEY=` 等のキーワード + - `.gitignore` / `.dockerignore` に上記が含まれているか + - Web サーバ設定で対象拡張子をブロック: + - nginx: `location ~ /\.(git|env|svn|DS_Store) { deny all; }` + - Apache: `` に `Require all denied` + +### 第2条: 不要になったページやウェブサイトを公開していない + +- **推奨対策**: 期間限定ページや管理不在ページを閉鎖 +- **自動チェック観点**: 公開ディレクトリの最終更新日が古いコンテンツの抽出、`robots.txt`/`sitemap.xml` の整合性。主に人手確認推奨 + +### 第3条: 「安全なウェブサイトの作り方」記載の脆弱性に対策している + +- **推奨対策**: 実装チェックリスト(本ファイルの Part 1)を網羅 +- **自動チェック観点**: 各個別 knowledge ファイル参照 + +### 第4条: ウェブアプリ構成ソフトウェアの脆弱性対策を定期的に実施 + +- **推奨対策**: フレームワーク・ライブラリの CVE 対応 +- **自動チェック観点**: + - 依存関係ファイル: `package.json` + `package-lock.json`, `requirements.txt`, `Pipfile.lock`, `Gemfile.lock`, `pom.xml`, `build.gradle`, `composer.json`/`composer.lock`, `go.sum`, `Cargo.lock` + - 脆弱性スキャナ統合: `npm audit`, `pip-audit`, `bundler-audit`, `OWASP Dependency-Check`, `Trivy`, `Snyk`, GitHub Dependabot + - lock ファイル欠如の検出 + - 非保守バージョン: EOL リスト(Node.js 14 以下、Python 3.7 以下、PHP 7.4 以下 等) + +### 第5条: 不必要なエラーメッセージを返していない + +- **推奨対策**: 本番でスタックトレース・SQL 文・パス情報を返さない +- **自動チェック観点**: + - フレームワーク設定: + - Rails: `config.consider_all_requests_local = false` + - Django: `DEBUG = False`、`ALLOWED_HOSTS` 設定 + - Spring Boot: `server.error.include-stacktrace=never`, `server.error.include-message=never` + - Express.js: 本番で `errorhandler` が詳細を返していない + - PHP: `display_errors = Off`, `display_startup_errors = Off`, `expose_php = Off` + - ASP.NET: `` または `` + - コード: + - `e.printStackTrace()` / `traceback.print_exc()` をレスポンスに混入 + - `res.send(err.stack)` / `res.json({error: err})` の生エラー返却 + - SQL エラーオブジェクトの JSON.stringify + - HTTP ヘッダ: `Server`, `X-Powered-By` を隠蔽(`server_tokens off;` / `expose_php = Off`) + +### 第6条: ウェブアプリケーションのログを保管し定期的に確認 + +- **推奨対策**: アクセス・認証・トランザクション・例外を構造化ログ出力 +- **自動チェック観点**: + - ロガー初期化: `Log4j`, `Logback`, `winston`, `pino`, `python logging`, `Monolog` + - 重要イベントログ: 認証成功/失敗、認可エラー、パスワード変更、権限変更、決済等 + - ログ出力先設定 + - **機微情報を出力していない**: `password`, `pwd`, `secret`, `token`, `authorization`, `cookie`, `cardNumber`, `cvv` をログ関数の引数に渡していないか + - ログローテーション: `logrotate.conf`, `log4j` の `RollingFileAppender` + +### 第7条: 通信内容の暗号化(HTTPS)ができている + +- **推奨対策**: 全面 HTTPS、TLS 1.2 以上、HSTS、HTTPS リダイレクト +- **自動チェック観点**: + - **HTTPS リダイレクト** (port 80 → 443): + - nginx: `return 301 https://$host$request_uri;` + - Apache: `RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]` + - Spring Boot: `server.ssl.enabled=true` + `security.require-ssl=true` + - Express: `if(!req.secure) return res.redirect('https://'+req.headers.host+req.url);` + - **HSTS ヘッダ**: + - `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload` + - nginx: `add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;` + - アンチパターン: 未設定、`max-age=0`、過小な `max-age`(86400 等) + - **TLS 設定**: + - `ssl_protocols TLSv1.2 TLSv1.3;`(`SSLv2/3`, `TLSv1`, `TLSv1.1` はアンチパターン) + - `ssl_ciphers` に `RC4`, `DES`, `3DES`, `MD5`, `NULL` を含まない + - `ssl_prefer_server_ciphers on;` + - **混在コンテンツ**: HTML/JS/CSS 中の `http://` 外部リソース URL + +### 第8条: 不正ログインの対策ができている + +- **推奨対策**: 強力なパスワード、ロックアウト、多要素認証、認証ログ +- **自動チェック観点**: + - **パスワードハッシュ**: + - 推奨: `bcrypt`, `scrypt`, `argon2`, `PBKDF2` + - アンチパターン: `md5(`, `sha1(`, `sha256(password)` 等の単純ハッシュ/ソルトなし + - **ログイン試行制限**: `express-rate-limit`, `django-axes`, `rack-attack`, `bucket4j` 等 + - **多要素認証**: `otplib`, `pyotp`, `devise-two-factor` + - **Cookie 属性**: `Secure`, `HttpOnly`, `SameSite=Lax|Strict` + - **セッション固定化対策**: ログイン成功後のセッション ID 再発行 + - **CAPTCHA / reCAPTCHA** 等のボット対策 + - **エラーメッセージ抽象化**: 「ユーザ ID またはパスワードが違います」 + +--- + +## B. ウェブサーバのセキュリティ対策(第9〜第14条) + +### 第9条: OS・サーバソフトウェア・ミドルウェアをバージョンアップしている + +- **自動チェック観点**: + - Dockerfile: `FROM ubuntu:18.04` 等 EOL イメージ + - `apt-get update` 後の `--no-install-recommends` + - `:latest` タグの使用は警告 + - CI/CD で `trivy image`, `docker scan` 等のスキャン + +### 第10条: 不要なサービス・アプリケーションがない + +- **自動チェック観点**: + - Dockerfile / docker-compose で公開ポートが必要最小限か + - nginx/Apache の有効モジュール一覧 + - `apt-get install` パッケージリストの妥当性 + +### 第11条: 不要なアカウントが登録されていない + +- **自動チェック観点**: + - シードデータ・マイグレーションのテストアカウント: `admin / admin`, `test / test`, `root / root`, `guest / guest` + - サンプルユーザ作成スクリプトの本番投入チェック + - DB マイグレーションに `DEFAULT 'password'` のようなパターン + +### 第12条: 推測されやすい単純なパスワードを使用していない + +- **自動チェック観点**: + - 環境変数/設定ファイル中のパスワード強度: `DB_PASSWORD=password`, `JWT_SECRET=secret`, `ADMIN_PASS=admin123` + - ハードコードされた認証情報: `password = "..."`, `apiKey = "..."`, AWS アクセスキー `AKIA[0-9A-Z]{16}`、GitHub PAT 形式 + - パスワードポリシー実装 + +### 第13条: ファイル・ディレクトリのアクセス制御を適切に設定 + +- **自動チェック観点**: + - Dockerfile での `USER` 指示(`USER root` のままはアンチパターン) + - ファイルアップロード先ディレクトリでの実行権限禁止: + - nginx: `location /uploads/ { location ~ \.(php|jsp|aspx|cgi)$ { deny all; } }` + - Apache: ` php_admin_flag engine off ` + - `chmod 777` のようなパーミッション設定 + - 静的ファイルディレクトリで `Options -Indexes`/`autoindex off;` + +### 第14条: ウェブサーバのログを保管し定期的に確認 + +- **自動チェック観点**: + - nginx: `access_log /var/log/nginx/access.log;` `error_log ... warn;` + - Apache: `CustomLog`, `ErrorLog` 設定 + - `error_log` の `level` が `info` 以上 + - ログローテーション: `logrotate.d/*` + - DB スローログ/監査ログ設定の有無 + +--- + +## C. ネットワークのセキュリティ対策(第15〜第18条) + +### 第15条: 境界で不要な通信を遮断(ルータ等) + +- **自動チェック観点**: + - クラウド設定(IaC): + - Terraform / CloudFormation の Security Group で `0.0.0.0/0` 全開放(SSH 22, RDP 3389, DB 3306/5432) + - `direction = "ingress"` + `cidr = "0.0.0.0/0"` + `port = 22` + - Kubernetes NetworkPolicy + +### 第16条: ファイアウォールで通信を適切にフィルタリング + +- **自動チェック観点**: IaC の `egress` ルール過剰開放、WAF / Cloud Firewall 設定 + +### 第17条: ウェブサーバへの不正な通信を検知・遮断(IDS/IPS/WAF) + +- **自動チェック観点**: + - ModSecurity / OWASP CRS(nginx の `modsecurity on;`) + - CloudFront/ALB + AWS WAF の関連付け + - 「人手確認推奨」 + +### 第18条: ネットワーク機器のログを保管し定期的に確認 + +- **自動チェック観点**: VPC Flow Logs / ALB Access Logs / WAF Logs 設定の有無 + +--- + +## D. その他のセキュリティ対策(第19〜第20条) + +### 第19条: クラウド利用における責任範囲を把握し対策を実施 + +- **自動チェック観点**: + - クラウドプロバイダ別ベストプラクティス遵守: + - S3 バケット: `BlockPublicAccess`, `Versioning`, `Encryption` + - RDS: `StorageEncrypted`, `PubliclyAccessible=false` + - IAM Role: 最小権限(`*` ワイルドカードの濫用検出) + - IaC ツール: `tfsec`, `checkov`, `cfn-nag` + +### 第20条: 定期的にセキュリティ検査(診断)・監査している + +- **自動チェック観点**: + - CI/CD パイプライン: + - SAST: CodeQL, Semgrep, SonarQube + - DAST: OWASP ZAP, Burp Suite + - SCA: 依存関係スキャン + - シークレットスキャン: gitleaks, truffleHog + - ペネトレーションテスト実施記録(人手確認) + +--- + +# Part 3: 自動チェック観点サマリ + +## A. 設定ファイル検査で検出可能 + +| 観点 | 対象ファイル | 検出パターン例 | +| --- | --- | --- | +| HTTPS 強制 | `nginx.conf`, `.htaccess`, `web.config`, `application.yml` | `return 301 https://`, `RewriteRule ... https` | +| HSTS | `nginx.conf`, ミドルウェア | `Strict-Transport-Security` ヘッダ付与 | +| TLS バージョン | `nginx.conf`, `ssl.conf` | `ssl_protocols TLSv1.2 TLSv1.3` | +| 弱い暗号スイート | `ssl_ciphers` | `RC4`, `DES`, `MD5`, `NULL` を含まない | +| サーバ情報非表示 | `nginx.conf`, `php.ini` | `server_tokens off;`, `expose_php = Off` | +| ディレクトリリスティング無効 | `nginx.conf`, Apache | `autoindex off;`, `Options -Indexes` | +| Cookie Secure 属性 | フレームワーク設定 | `session.cookie_secure=1`, `cookie.secure=true` | +| Cookie HttpOnly | 同上 | `cookie.httpOnly=true` | +| Cookie SameSite | 同上 | `cookie.sameSite=Lax\|Strict` | +| エラー詳細非表示 | `php.ini`, `production.rb`, `application.properties` | `display_errors=Off`, `include-stacktrace=never` | +| シークレットの公開ディレクトリ排除 | `.gitignore`, ディレクトリ構造 | `.env` 等を `public/` 配下に置かない | +| 安全な SG | Terraform/CloudFormation | `0.0.0.0/0` + 重要ポートの組合せ検出 | + +## B. ソースコード静的解析で検出可能 + +| 観点 | 検出パターン | +| --- | --- | +| 危険ハッシュ関数 | `md5(`, `sha1(`, `crypto.createHash('md5')` でパスワード処理 | +| 平文パスワード保存 | DB カラムへの `password` 直接保存 | +| プレースホルダ未使用 SQL | 文字列連結 SQL | +| シェル実行 | `exec()`, `system()`, `Runtime.exec()`, `subprocess.call(shell=True)` | +| パストラバーサル | `open(req.params.file)`, `File.read(userInput)` | +| XSS 危険関数 | `innerHTML =`, `document.write(`, `v-html`, `dangerouslySetInnerHTML` | +| エスケープ未使用 | `{{{ }}}`, `<%= raw %>`, `\| safe` | +| CSRF トークン未生成 | `csrf: false`, `csrf_exempt`, `http.csrf().disable()` | +| シークレットハードコード | API キー、AWS キー、JWT 秘密鍵 | +| ログ機微情報出力 | `logger.info(password)`, `console.log(token)` | + +## C. 人手確認が必要な項目 + +| 項目 | 理由 | +| --- | --- | +| 1-(iv) DB アカウント権限の妥当性 | 業務要件次第 | +| 5-(vi) HTML サニタイザ設定の妥当性 | 許可タグ・属性のレビュー | +| 6-(ii) 重要操作の通知メール | 「重要操作」の定義レビュー | +| 9-(i)-b 重要操作前の再認証 | 業務要件依存 | +| 9-(ii) マウスのみ操作不可 | UI/UX 設計レビュー | +| 11-(i) 認証要否そのものの判断 | 機能仕様レビュー | +| 11-(ii) 認可ロジックの完全性 | データモデル・所有関係の理解必要 | +| 10 ネイティブ部分の最小化 | アーキテクチャレビュー | +| 第2条 不要ページの閉鎖判断 | 業務要件依存 | +| 第8条 MFA 適用方針/ロックアウト閾値 | 業務要件依存 | +| 第11条 アカウント棚卸し | 業務要件依存 | +| 第19条 責任分界点の理解 | 契約・運用 | +| 第20条 ペネトレーションテスト実施 | 運用プロセス | +| WAF/IDS の検知ルール妥当性 | セキュリティ運用 | +| DNS 設定の正当性(DNSSEC, SPF/DKIM/DMARC) | DNS 運用 | + +--- + +# Part 4: HTTP セキュリティヘッダ推奨リスト + +| ヘッダ | 推奨値 | 関連 | +| --- | --- | --- | +| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | 第7条 | +| `Content-Security-Policy` | プロジェクト個別。最低でも `default-src 'self'` | XSS(5-(x)) | +| `X-Content-Type-Options` | `nosniff` | 第3条 | +| `X-Frame-Options` | `DENY` または `SAMEORIGIN` | クリックジャッキング(9-(i)-a) | +| `Referrer-Policy` | `strict-origin-when-cross-origin` | プライバシ | +| `Permissions-Policy` | 機能を必要分のみ許可 | プライバシ | +| `Cache-Control` | 機微ページで `no-store` | 情報漏洩 | +| `Server` / `X-Powered-By` | 出力しない | 第5条 | + +--- + +# Part 5: 関連カテゴリ(Skill ルールタグ) + +- `injection.sql` / `injection.os` / `injection.header` / `injection.mail` +- `path.traversal` +- `session.cookie` / `session.fixation` +- `xss.output_encoding` / `xss.csp` +- `csrf.token` / `csrf.referer` +- `clickjacking.frame_options` +- `buffer_overflow.lang` +- `access_control.authz` +- `tls.https` / `tls.hsts` +- `password.hash` / `password.policy` +- `error.message` / `log.audit` +- `file.upload` / `file.permission` +- `dependency.cve` + +--- + +# Part 6: CWE 対応表(巻末 p.109〜) + +| カテゴリ | 代表 CWE | +| --- | --- | +| SQL i | CWE-89 | +| OS コマンド i | CWE-78 | +| パストラバーサル | CWE-22 | +| セッション管理不備 | CWE-330 / CWE-384 / CWE-522 / CWE-614 | +| XSS | CWE-79 | +| CSRF | CWE-352 | +| HTTP ヘッダ i | CWE-113 | +| メールヘッダ i | CWE-93 | +| クリックジャッキング | 直接対応 CWE なし(CWE-1021) | +| バッファオーバーフロー | CWE-119 / CWE-787 | +| アクセス制御欠落 | CWE-264 / CWE-287 / CWE-639 | + +--- + +# Part 7: 第7版で追加・更新された項目 + +- 第1章: クリックジャッキング、バッファオーバーフローの脆弱性解説を追加 +- 第1章: XSS への対策方法、各脆弱性で紹介している届出状況、参考 URL を更新 +- 第2章: ウェブサイトにおけるパスワードの管理方法を追加(ソルト付きハッシュ、ストレッチング) +- 第2章: 通信経路の暗号化(HSTS 含む)、DNS 対策、参考 URL を更新 + +--- + +# Part 8: 第2章「ウェブサイトの安全性向上のための取り組み」要点 + +20ヶ条と重複するが、より詳細な実装ガイダンスがある章。 + +## 2.3 ネットワーク盗聴への対策 + +- 重要情報を扱うページの **完全 HTTPS 化** +- HSTS(`Strict-Transport-Security`)の利用 +- セッション Cookie の `secure` 属性 +- メール通知ではなく HTTPS ページでの重要情報表示 +- メール経路の暗号化(S/MIME, PGP) + +## 2.4 フィッシング詐欺を助長しないための対策 + +- EV SSL 証明書の取得(運用面) +- 子フレームの URL を外部パラメータから動的生成しない +- リダイレクト先パラメータは **自サイトドメインのみ許可** + - 自動チェック観点: `redirect_to params[:url]` のような外部入力→リダイレクト + +## 2.5 パスワードに関する対策 + +| 項目 | 自動チェック観点 | +| --- | --- | +| 初期パスワードを推測困難な文字列で発行 | パスワード生成関数の `crypto`/`secrets` 系利用 | +| パスワード変更時の現行パスワード要求 | パスワード変更エンドポイントでの現行パスワード検証コード | +| 認証応答メッセージの抽象化 | エラーメッセージが「ユーザ ID またはパスワードが違います」 | +| パスワード入力フィールドの伏字 | HTML フォームの `` 使用 | +| **ソルト付きハッシュで保管** | bcrypt/argon2/scrypt/PBKDF2 の使用、`md5/sha1/sha256(password)` のアンチパターン検出 | + +## 2.6 WAF によるウェブアプリケーションの保護 + +- 実装面の対策とは別の防御層として WAF を導入 +- 自動チェック観点: ModSecurity / OWASP CRS、AWS WAF、Cloudflare WAF の関連付け(IaC) + +--- + +## 関連ルール ID(運用 + 実装チェックリスト統合) + +- IPA-OPS-01-PUBLIC-FILE: 公開すべきでないファイル(第1条) +- IPA-OPS-02-DEAD-PAGE: 不要ページ放置(第2条) +- IPA-OPS-03-SWS-COVERAGE: 安全なウェブサイトの作り方記載項目の網羅(第3条) +- IPA-OPS-04-CVE-DEP: 依存ライブラリの CVE(第4条) +- IPA-OPS-05-ERROR-MSG: エラー詳細露出(第5条) +- IPA-OPS-06-APP-LOG: アプリログ設定(第6条) +- IPA-OPS-07-HTTPS-HSTS: HTTPS / HSTS(第7条) +- IPA-OPS-08-LOGIN-DEFENSE: 不正ログイン対策(第8条) +- IPA-OPS-09-SW-UPDATE: OS/ミドルウェア更新(第9条) +- IPA-OPS-10-MINIMAL-SVC: 不要サービス(第10条) +- IPA-OPS-11-NO-DEFAULT-ACCT: デフォルトアカウント(第11条) +- IPA-OPS-12-PWD-POLICY: パスワード強度(第12条) +- IPA-OPS-13-FILE-PERM: ファイル権限(第13条) +- IPA-OPS-14-WEB-LOG: ウェブサーバログ(第14条) +- IPA-OPS-15-EDGE-FW: 境界遮断(第15条) +- IPA-OPS-16-FW-RULE: FW フィルタリング(第16条) +- IPA-OPS-17-WAF: WAF/IDS/IPS(第17条) +- IPA-OPS-18-NW-LOG: ネットワーク機器ログ(第18条) +- IPA-OPS-19-CLOUD: クラウド責任分界(第19条) +- IPA-OPS-20-AUDIT: 定期検査・監査(第20条) + +## 参考 + +- IPA「安全なウェブサイトの運用管理に向けての20ヶ条」: https://www.ipa.go.jp/security/vuln/websecurity/sitecheck.html +- IPA「安全なウェブサイトの作り方」改訂第7版 PDF: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017316.pdf +- IPA「セキュリティ実装チェックリスト」(xlsx): https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000044403.xlsx +- IPA「CWE 概説」: https://www.ipa.go.jp/security/vuln/CWE.html +- IPA「脆弱性関連情報の届出」: https://www.ipa.go.jp/security/vuln/report/index.html diff --git a/skills/ipa-security-check/knowledge/safe_sql_details.md b/skills/ipa-security-check/knowledge/safe_sql_details.md new file mode 100644 index 0000000..aedfe24 --- /dev/null +++ b/skills/ipa-security-check/knowledge/safe_sql_details.md @@ -0,0 +1,775 @@ +--- +name: safe_sql_details +ipa_document: 安全なSQLの呼び出し方(「安全なウェブサイトの作り方」別冊) +ipa_section: "全章(第1〜5章 + 付録A)" +ipa_page: "1-40" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017320.pdf +cwe: CWE-89 +--- + +# 安全な SQL の呼び出し方(IPA 別冊) + +## 出典 + +- 文書名: 安全なSQLの呼び出し方(「安全なウェブサイトの作り方」別冊) +- 版: 第1版 / 40ページ / 2010年3月18日 +- 発行者: 独立行政法人 情報処理推進機構(IPA) +- 執筆者: 徳丸 浩、永安 佑希允、相馬 基邦、勝海 直人、高木 浩光(産業技術総合研究所) +- PDF URL: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017320.pdf +- 関連 URL: https://www.ipa.go.jp/security/vuln/websecurity.html + +本ファイルは「安全な SQL の呼び出し方」全章を、自動チェック観点向けに統合・整理したもの。 + +--- + +## 1. SQL の構造とリテラル(第1〜2章) + +### 1.1 SQL 文の構成要素 + +```sql +SELECT a,b,c FROM atable WHERE name='YAMADA' and age>=20 +``` + +| 要素 | 例 | +|---|---| +| キーワード(予約語) | `SELECT`, `FROM`, `WHERE`, `AND` | +| 演算子など | `=`, `>=`, `,` | +| 識別子 | `a`, `b`, `c`, `atable`, `name`, `age` | +| リテラル | `'YAMADA'`, `20` | + +### 1.2 リテラルの種類 + +- **文字列リテラル**: `'情報処理推進機構'`、`'052312'`、`'O''Reilly'` +- **数値リテラル**: `20`、`-17`、`0`、`3.14159`、`6.0221415E+23` +- **日時リテラル**: `DATE '2009-11-04'`、`TIME '13:59:26'` +- **論理値リテラル** + +### 1.3 文字列リテラルのエスケープ + +文字列リテラル中にシングルクォートが現れる場合、シングルクォートを重ねて表現するのが SQL の文法。 + +```sql +-- NG: 構文エラー +SELECT * FROM employee WHERE name = 'O'Reilly' + +-- OK +SELECT * FROM employee WHERE name = 'O''Reilly' +``` + +エスケープが必要な文字は **データベースエンジンの種類や設定によって異なる**。 + +### 1.4 数値リテラル + +数値リテラルはクォートしない。JIS X 3005 / ISO/IEC 9075 規格では、数値リテラルの次には記号・空白・コメントが続かなければならない。 + +```sql +-- 規格準拠 +SELECT * FROM employee WHERE age >= 25--comment + +-- 規格違反(ただし Microsoft SQL Server や PostgreSQL は受け入れる) +SELECT * FROM employee WHERE age >= 25and age <= 60 +``` + +### 1.5 SQL インジェクションの原因 + +#### 文字列リテラルに対するインジェクション + +```perl +$q = "SELECT * FROM atable WHERE id='$id'"; +``` + +`$id` に `';DELETE FROM atable--` を与えると: +```sql +SELECT * FROM atable WHERE id='';DELETE FROM atable--' +``` + +#### 数値リテラルに対するインジェクション + +```perl +$q = "SELECT * FROM atable WHERE id=$id"; +``` + +`$id` に `0;DELETE FROM atable` を与えると: +```sql +SELECT * FROM atable WHERE id=0;DELETE FROM atable +``` + +### 1.6 安全な SQL 呼び出しの要件 + +- **文字列リテラルに対しては、エスケープすべき文字をエスケープする** +- **数値リテラルに対しては、数値以外の文字を混入させない** + +### 1.7 SQL 呼び出し方の分類 + +| 方式 | 評価 | +|---|---| +| 文字列連結 + エスケープなし | 危険 | +| 文字列連結 + quote メソッド | DB 連動が正しいライブラリでは可。ただし数値型のサポート不十分な実装あり | +| 動的プレースホルダ | ライブラリの実装に依存。文字エンコーディング起因の脆弱性が発生し得る | +| 静的プレースホルダ | SQL 構文がバインド前に確定する原理的に最も安全な方式(**推奨**) | + +--- + +## 2. バインド機構(第3章) + +### 2.1 静的プレースホルダ(Prepared Statement) + +JIS/ISO 規格における「準備された文(Prepared Statement)」。 + +**仕組み**: + +1. プレースホルダ記号 `?` のままの SQL 文を、DB エンジン側に事前に送信 +2. DB エンジン側で構文解析などを完了 +3. SQL 実行時に、実際のパラメータの値だけを DB エンジンに送信 +4. DB エンジン側でバインド処理 + +**セキュリティ上の利点(最も安全)**: + +- SQL 文の構文が**バインド前に確定**しているため、後から SQL 構文が変化することがない +- パラメータの値がリテラルの外にはみ出す現象が起きない +- プレースホルダに渡す文字列をクォートして記述する必要がない(**シングルクォートのエスケープ処理も不要**) +- 数値リテラルもそのまま適切にバインドされる + +→ **原理的に SQL インジェクションの脆弱性が生じない** + +**注意事項**: + +- DB エンジンやライブラリによってはサポートされていない場合がある +- ライブラリによっては `prepare` のような名称でも実体が動的プレースホルダになっているケースがあるため、設定(明示指定)に注意 + +### 2.2 言語別の静的プレースホルダ指定方法 + +| 環境 | 静的プレースホルダ指定 | +|---|---| +| Java + Oracle (ojdbc6) | 常に静的(指定不要) | +| Java + MySQL | 接続 URL に `useServerPrepStmts=true` | +| Perl + DBD::mysql | 接続文字列に `mysql_server_prepare=1` | +| PHP + MDB2 + PostgreSQL | 常に静的(指定不要) | +| ASP.NET + SQL Server | 常に静的(指定不要) | + +### 2.3 動的プレースホルダ + +プレースホルダ機能はあるが、バインド処理を**データベースエンジン側ではなく、アプリケーション側のライブラリ内で実行**する方式。 + +俗に「クライアントサイドのプリペアドステートメント」と呼ばれるが、JIS/ISO で規定された「準備された文」ではない。 + +**特徴**: + +- プレースホルダを用いることで、文字列連結に比べてエスケープ漏れを防止できる +- バインド処理を実現するライブラリの実装によっては、**SQL 構文を変化させる SQL インジェクションを許してしまう脆弱な実装**の可能性は否定できない +- 特に、**文字エンコーディングの扱いが不適切な実装**では、Shift_JIS や EUC-JP 使用時に SQL インジェクション脆弱性が発生し得る(付録 A.3 / 旧 MySQL Connector/J 5.1.7 以前) + +### 2.4 静的 vs 動的 比較表 + +| 比較項目 | 静的プレースホルダ | 動的プレースホルダ | +|---|---|---| +| バインド処理の場所 | DB エンジン | クライアント側ライブラリ | +| SQL 構文の確定タイミング | バインド前 | バインド後 | +| 実行効率 | 高い | やや劣る | +| エスケープ漏れリスク | なし | ライブラリ依存 | +| 文字エンコーディング起因の脆弱性 | 発生しない | 発生し得る | +| 構文として安全か | 原理的に安全 | 実装が正しければ安全 | +| 推奨度 | **最推奨** | 静的が使えない時のみ | + +--- + +## 3. エスケープ処理(第4章 / 付録 A.1) + +### 3.1 データベース別エスケープ対象文字 + +#### 標準(JIS/ISO 規格に準拠) + +| エスケープ対象 | エスケープ方法 | +|---|---| +| `'` | `''` | + +#### MySQL のデフォルト設定 / PostgreSQL のデフォルト設定 + +| エスケープ対象 | エスケープ方法 | +|---|---| +| `'` | `''`(`\'` でもよい) | +| `\` | `\\` | + +シングルクォート以外に **バックスラッシュ `\`** がエスケープ用のメタ文字として解釈されるため、文字列リテラル中にバックスラッシュが含まれる場合は、バックスラッシュ自身のエスケープも必要。 + +#### バックスラッシュをメタ文字扱いしない設定 + +| DB | オプション | 効果 | +|---|---|---| +| MySQL | `NO_BACKSLASH_ESCAPES` | バックスラッシュをエスケープ用のメタ文字として扱わない | +| PostgreSQL | `standard_conforming_strings=on` | バックスラッシュをエスケープ用のメタ文字として扱わない | + +### 3.2 バックスラッシュエスケープを怠った場合の脆弱性例 + +入力 `\';DELETE FROM atable--` に対してシングルクォートだけエスケープすると: + +``` +\'';DELETE FROM atable-- +``` + +このまま SQL に渡すと: + +```sql +SELECT * FROM atable WHERE a='\'';DELETE FROM atable--' +``` + +`\'` が「シングルクォートのエスケープ」と見なされ、続くシングルクォートで文字列リテラルが終端し、`;DELETE` 以降がリテラル外にはみ出して実行される。 + +### 3.3 quote メソッドの活用 + +| 言語/ライブラリ | 関数/メソッド | +|---|---| +| Perl DBI | `$dbh->quote(...)` | +| PHP Pear::MDB2 | `$db->quote($v, 'text')` | +| PHP PDO | `$pdo->quote(...)` | + +#### Pear::MDB2 quote メソッドの戻り値例 + +| データ | 型指定 | 戻り値 | +|---|---|---| +| `abc` | `'text'` | `'abc'` | +| `O'Reilly` | `'text'` | `'O''Reilly'` | +| `-123` | `'decimal'` | `-123` | +| `123abc` | `'decimal'` | `123` | +| `-123` | `'integer'` | `-123` (整数型) | +| `123abc` | `'integer'` | `123` (整数型) | + +非数値文字を含む入力は、数値部分のみが抽出されて返るため、SQL 構文エラー&SQL インジェクションを防止できる。 + +### 3.4 quote メソッドが正しく動作しないケース(注意) + +- **Perl + DBD::mysql の quote メソッド(数値型指定)**: 入力文字列が数値として妥当かのチェックを行わず、数値変換もせず、**入力文字列をそのまま返す**。SQL インジェクション対策として使用できない。 + ```perl + $dbh->quote("1 or 1=1", SQL_INTEGER); # → "1 or 1=1" を返す(NG) + ``` +- **Perl + DBD::PgPP (Pure Perl PostgreSQL driver) バージョン 0.08**: 動的プレースホルダと quote メソッドで、データベースエンジンの種類や設定に連動したエスケープが行われない。 + +### 3.5 識別子(テーブル名・カラム名・ソート順)の動的組み立て + +本書には識別子の動的組み立てについての**専用節は無い**。ただし以下の重要な前提が読み取れる: + +- 本書の「リテラル」「プレースホルダ」「quote メソッド」は **値(リテラル)** に対する仕組みであり、**識別子には適用できない** +- 識別子を動的に組み立てる必要がある場合、プレースホルダもエスケープ関数も使用できないため、**ホワイトリスト方式で検証することが必須** + +#### 推奨実装 + +```php +// PHP: カラム名のホワイトリスト検証 +$allowed_columns = ['id', 'name', 'created_at']; +$order_col = in_array($_GET['order'], $allowed_columns, true) + ? $_GET['order'] + : 'id'; +$sql = "SELECT * FROM users ORDER BY $order_col"; +``` + +#### 危険な実装 + +```php +// NG +$sql = "SELECT * FROM users ORDER BY " . $_GET['order']; + +// NG: プレースホルダは識別子には効かない +$stmt = $pdo->prepare("SELECT * FROM users ORDER BY ?"); +$stmt->execute([$_GET['order']]); +``` + +### 3.6 LIKE 述語のエスケープ + +```sql +-- ANSI 標準: ESCAPE 句で明示 +SELECT * FROM atable WHERE name LIKE '50!%' ESCAPE '!' +``` + +### 3.7 エスケープ処理の落とし穴サマリ + +| 落とし穴 | 対策 | +|---|---| +| シングルクォートのみエスケープし、バックスラッシュをエスケープしない | DB エンジンの設定に応じて両方をエスケープ。`quote` メソッドを使う | +| バックスラッシュ不要環境で `\\` にエスケープし、二重 `\` が DB に格納される | DB エンジンの設定を確認 | +| 数値リテラルを文字列としてエスケープのみ実施 | 数値はキャスト/型検証で混入させない | +| `addslashes()` 等の言語汎用エスケープ関数を使う | DBMS 連動の `quote` / `mysqli_real_escape_string` 等を使う | +| Shift_JIS 環境で多バイト文字の2バイト目に `0x5C` を含む場合の誤エスケープ | UTF-8 にする・DBMS 連動のエスケープ関数を使う | +| 識別子(テーブル名・カラム名)にエスケープ関数を適用 | ホワイトリスト検証 | + +--- + +## 4. 言語別・DB 別実装ガイド(第5章) + +### 調査結果サマリ表 + +| 観点 | Java + Oracle | PHP + MDB2 + PostgreSQL | Perl + MySQL | Java + MySQL | ASP.NET + SQL Server | +|---|---|---|---|---|---| +| プレースホルダの実装 | 静的のみ | 静的のみ | 静的または動的 | 静的または動的 | 静的のみ | +| 動的プレースホルダの処理 | - | - | 正しく処理される | 正しく処理(古いバージョン除く) | - | +| quote 処理(文字列) | - | 正しく処理される | 正しく処理される | - | - | +| quote 処理(数値) | - | 正しく処理される | **正しく処理されない** | - | - | +| 文字エンコーディング | UTF-8 固定 | 指定可能 | UTF-8 を明示可能 | 指定可能 | UTF-16 固定 | + +### 4.1 Java + Oracle(OK 例) + +```java +String sql = "SELECT * FROM atable WHERE name=?"; +PreparedStatement stmt = con.prepareStatement(sql); +stmt.setString(1, param); +ResultSet rs = stmt.executeQuery(); +``` + +### 4.2 PHP + Pear::MDB2 + PostgreSQL(OK 例) + +```php +$db = MDB2::connect('pgsql://username:password@hostname/dbname?charset=utf8'); +$stmt = $db->prepare('SELECT * FROM atable WHERE name=? and num=?', + array('text', 'integer'), + array('text', 'text', 'integer')); +$rs = $stmt->execute(array($name, $num)); +``` + +ポイント: + +1. `charset=utf8` を指定(Shift_JIS は SQL インジェクション問題が発生しやすいため避ける) +2. プレースホルダの**型を指定**する + +### 4.3 Perl + DBI + DBD::mysql + MySQL(OK 例) + +```perl +my $db = DBI->connect( + 'DBI:mysql:database=xxxx;host=xxxx;mysql_server_prepare=1;mysql_enable_utf8=1', + 'xxxx', 'xxxx'); +my $sql = 'SELECT * FROM antable WHERE num=? AND name=?'; +my $sth = $db->prepare($sql); +$sth->bind_param(1, $num, SQL_INTEGER); +$sth->bind_param(2, $name, SQL_VARCHAR); +my $rt = $sth->execute(); +``` + +ポイント: + +1. `mysql_server_prepare=1` で静的プレースホルダを指定 +2. `mysql_enable_utf8=1` で UTF-8 統一 +3. `bind_param` で型を明示する + +### 4.4 Java + JDBC (MySQL Connector/J) + MySQL(OK 例) + +```java +String url = "jdbc:mysql://HOSTNAME/DBNAME" + + "?user=USERNAME&password=PASSWORD" + + "&useUnicode=true&characterEncoding=utf8&useServerPrepStmts=true"; +Connection con = DriverManager.getConnection(url); +PreparedStatement stmt = con.prepareStatement("SELECT * FROM atable WHERE name=?"); +stmt.setString(1, param); +``` + +接続 URL の重要パラメータ: + +| パラメータ | 説明 | +|---|---| +| `useUnicode=true` | Unicode を使用 | +| `characterEncoding=utf8` | 接続の文字エンコーディングを UTF-8 に | +| `useServerPrepStmts=true` | 静的プレースホルダ(サーバサイド)を使用 | + +### 4.5 ASP.NET + ADO.NET + Microsoft SQL Server(OK 例) + +```vb +sqlStr = "select * from aTable where name=@s1" +dbcmd = New SqlCommand(sqlStr, dbcon) +Dim p1 As SqlParameter = New SqlParameter("@s1", param) +dbcmd.Parameters.Add(p1) +``` + +--- + +## 5. 文字エンコーディングの注意点(付録 A.2〜A.5) + +### 5.1 Shift_JIS による SQL インジェクション(付録 A.2) + +入力 `表';DELETE FROM atable--` の `表` (0x95 0x5C) が、文字エンコーディングを考慮しないエスケープで `0x5C` がバックスラッシュとして解釈され、Shift_JIS 解釈で: + +```sql +SELECT * FROM atable WHERE a='表\'';DELETE FROM atable--' +``` + +`\'` が「シングルクォートのエスケープ」と解釈され、SQL インジェクションが成立。 + +**対策**: + +- データベースエンジン、ライブラリ、プログラミング言語の文字列処理が文字エンコーディングを正しく扱えるものを使用 +- **Shift_JIS の使用を避ける** + +### 5.2 Unicode による SQL インジェクション(付録 A.3 / JVN#59748723) + +**MySQL Connector/J 5.1.7 以前** で、以下の条件で SQL インジェクションが発生: + +- MySQL Connector/J 5.1.7 以前 +- 接続文字エンコーディングが **Shift_JIS あるいは EUC-JP** +- **動的プレースホルダ**を使用 + +`¥'or 1=1#` をバインドすると Unicode → Shift_JIS 変換で `¥` (U+00A5) が `\` (0x5C) に変換され、`\\'or 1=1#` として生成された SQL がインジェクション成立。 + +**対策(一つ以上、すべて推奨)**: + +- 接続文字エンコーディングに **Unicode (UTF-8) を指定** (`characterEncoding=utf8`) +- **静的プレースホルダを使用** (`useServerPrepStmts=true`) +- MySQL Connector/J を **最新版に更新** + +### 5.3 Oracle データベースを Unicode で作成(付録 A.4) + +- Oracle はデータベース単位で文字エンコーディングを指定 +- **既存データベースの文字エンコーディングは変更不可** +- デフォルト: `JA16SJISTILDE`(Shift_JIS) +- **推奨**: `AL32UTF8`(Unicode) + +### 5.4 Microsoft SQL Server と文字コード(付録 A.5) + +- Microsoft .NET 内部処理: UTF-16 +- SQL Server テーブル文字列格納: サーバの動作環境のコードページ(日本語環境では CP932) +- Unicode 文字列の格納には `nchar`/`nvarchar` 型を使い、リテラルに `N'...'` 形式を前置 + +```sql +CREATE TABLE aTable (name NVARCHAR(30), city NVARCHAR(30)); +INSERT INTO aTable VALUES (N'佐藤', N'横浜市'); +``` + +### 5.5 文字エンコーディング推奨設定まとめ + +| 環境 | 推奨設定 | +|---|---| +| 全般 | **Shift_JIS の使用を避ける**。UTF-8 / UTF-16 で統一 | +| PHP + PostgreSQL | DSN に `charset=utf8` | +| Perl + MySQL | 接続文字列に `mysql_enable_utf8=1` | +| Java + MySQL | 接続 URL に `useUnicode=true&characterEncoding=utf8` | +| Oracle DB | データベース作成時に `AL32UTF8` を指定 | +| Microsoft SQL Server | Unicode データは `nvarchar`/`nchar`、リテラルには `N'...'` | + +--- + +## 6. データベースアカウントの権限 + +本書には専用節はないが、IPA「安全なウェブサイトの作り方」本編 1-(iv) の保険的対策。 + +### 一般原則(最小権限の原則) + +- アプリが DB に接続する際のアカウントには、**業務上必要最小限の権限のみ付与する** +- `DROP TABLE`、`CREATE TABLE`、`GRANT` などの DDL/管理権限は通常付与しない +- 参照のみのアカウント、更新のみのアカウントなど、機能別にアカウントを分離する +- アプリケーションサーバと DB サーバ間の接続は信頼できるネットワーク経由とする + +--- + +## 7. 自動チェック観点(重要・第6章相当) + +### 7.1 検出すべき危険パターン(NG例) + +#### 文字列連結による SQL 組み立て + +**PHP**: +```php +// NG: 直接連結 +$name = $_POST['name']; +$sql = "SELECT * FROM employee WHERE name='" . $name . "'"; + +// NG: 変数展開 +$sql = "SELECT * FROM employee WHERE name='$name'"; + +// NG: sprintf +$sql = sprintf("SELECT * FROM employee WHERE id=%s", $_GET['id']); + +// NG: PDO::prepare でも変数連結 +$stmt = $pdo->prepare("SELECT * FROM users WHERE name='" . $name . "'"); +``` + +**Perl**: +```perl +# NG: 文字列連結 (IPA本書 2.5.1) +$q = "SELECT * FROM atable WHERE id='$id'"; + +# NG: 数値リテラル文字列連結 (IPA本書 2.5.2) +$q = "SELECT * FROM atable WHERE id=$id"; +``` + +**Java**: +```java +// NG: 文字列連結 +String sql = "SELECT * FROM users WHERE name='" + name + "'"; + +// NG: Statement.executeQuery で動的 SQL +Statement stmt = con.createStatement(); +ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE id=" + userId); +``` + +**Ruby**: +```ruby +# NG: 文字列展開 +sql = "SELECT * FROM users WHERE name='#{params[:name]}'" + +# NG (Rails): where 句に文字列展開 +User.where("name = '#{params[:name]}'") +``` + +**C#**: +```csharp +// NG: 文字列補間 +string sql = $"SELECT * FROM users WHERE name='{name}'"; +``` + +#### quote / エスケープ関数の誤用 + +```php +// NG: addslashes は DB エンジン連動でない、Shift_JIS 問題あり +$name = addslashes($_POST['name']); +$sql = "SELECT * FROM users WHERE name='$name'"; + +// NG: 数値型に文字列用エスケープを適用 +$id = mysqli_real_escape_string($conn, $_GET['id']); +$sql = "SELECT * FROM users WHERE id=$id"; // クォートしないため id=1 or 1=1 が通る + +// NG: htmlspecialchars は SQL 用エスケープではない +$name = htmlspecialchars($_POST['name']); +``` + +```perl +# NG: DBD::mysql の quote (数値型) は入力をそのまま返す +$dbh->quote("1 or 1=1", SQL_INTEGER); +``` + +#### 動的プレースホルダ + Shift_JIS / EUC-JP + +```java +// NG: 古い MySQL Connector/J + Shift_JIS + 動的プレースホルダ (JVN#59748723) +Connection con = DriverManager.getConnection( + "jdbc:mysql://HOST/DB?useUnicode=true&characterEncoding=sjis"); +// useServerPrepStmts が指定されておらず動的プレースホルダ +``` + +#### 識別子の直接埋め込み + +```php +// NG: ORDER BY の列名を直接埋め込み +$sql = "SELECT * FROM users ORDER BY " . $_GET['sort']; + +// NG: テーブル名を直接埋め込み +$table = $_GET['table']; +$sql = "SELECT * FROM $table"; +``` + +### 7.2 推奨パターン(OK例) + +**PHP (PDO)**: +```php +$stmt = $pdo->prepare('SELECT * FROM users WHERE name = :name AND age = :age'); +$stmt->execute(['name' => $name, 'age' => $age]); +``` + +**PHP (mysqli)**: +```php +$stmt = $mysqli->prepare('SELECT * FROM users WHERE id = ? AND name = ?'); +$stmt->bind_param('is', $id, $name); // i=integer, s=string +$stmt->execute(); +``` + +**Java**: +```java +PreparedStatement stmt = con.prepareStatement("SELECT * FROM atable WHERE name=?"); +stmt.setString(1, name); +stmt.setInt(1, id); +``` + +**ASP.NET**: +```csharp +var cmd = new SqlCommand("SELECT * FROM users WHERE name=@name AND id=@id", conn); +cmd.Parameters.AddWithValue("@name", name); +cmd.Parameters.Add("@id", SqlDbType.Int).Value = id; +``` + +**Ruby**: +```ruby +User.where("name = ?", params[:name]) +User.where(name: params[:name]) +``` + +**識別子のホワイトリスト**: +```php +$allowed = ['id', 'name', 'created_at']; +$col = in_array($_GET['sort'], $allowed, true) ? $_GET['sort'] : 'id'; +$sql = "SELECT * FROM users ORDER BY $col"; +``` + +**数値キャスト**: +```php +$id = (int)$_GET['id']; +$sql = "SELECT * FROM users WHERE id=$id"; +``` + +### 7.3 grep / 正規表現での検出ルール候補 + +#### PHP + +```regex +# 変数展開 + シングルクォート +"\bSELECT\b.*'.*\$\w+.*'.*" + +# 文字列連結による SQL 組み立て +"\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*\"\s*\.\s*\$\w+" + +# クエリ系関数への文字列連結引数 +"(mysql_query|mysqli_query|pg_query)\s*\([^)]*\.\s*\$" + +# addslashes / htmlspecialchars を経由した SQL 構築 +"addslashes\s*\(.*\).*(SELECT|INSERT|UPDATE|DELETE)" + +# PDO::prepare 引数に文字列連結 +"->prepare\s*\(\s*\"[^\"]*\"\s*\.\s*\$" +``` + +#### Perl + +```regex +# 文字列リテラル内変数展開(SQL 様) +"\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*\\\$\w+" + +# qq{...} 内変数展開 +"qq[{(\[].*?(SELECT|INSERT|UPDATE|DELETE).*?\\\$\w+" + +# $dbh->do / prepare に変数連結 +"->(do|prepare)\s*\(.*\.\s*\\\$" + +# quote(..., SQL_INTEGER) - DBD::mysql で危険 +"->quote\s*\([^,]+,\s*SQL_INTEGER\s*\)" +``` + +#### Java + +```regex +# SQL 文字列 + 変数連結 +"\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*\"\s*\+\s*\w+" + +# Statement.executeQuery / executeUpdate に動的 SQL +"Statement\s+\w+\s*=\s*[^;]*createStatement" +"\.executeQuery\s*\(\s*\"[^\"]*\"\s*\+" + +# String.format で SQL +"String\.format\s*\(\s*\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)" + +# 古い MySQL Connector/J で危険な設定 +"characterEncoding=sjis" +"characterEncoding=ujis" +"jdbc:mysql:(?!.*useServerPrepStmts=true)" +``` + +#### Ruby + +```regex +# 文字列展開 #{} を含む SQL 文字列 +"\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*#\{" + +# Rails where に文字列展開 +"\.where\s*\(\s*\"[^\"]*#\{" + +# raw / unsafe な execute +"\.execute\s*\(\s*\"[^\"]*#\{" +"\.find_by_sql\s*\(\s*\"[^\"]*#\{" +``` + +#### C# / VB.NET + +```regex +# C# 文字列連結 +"\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*\"\s*\+\s*\w+" + +# C# 文字列補間 +"\$\"[^\"]*(SELECT|INSERT|UPDATE|DELETE)[^\"]*\{" + +# SqlCommand コンストラクタに動的 SQL +"new\s+SqlCommand\s*\(\s*\"[^\"]*\"\s*\+" +``` + +#### 識別子直接埋め込み(全言語共通) + +```regex +# ORDER BY に変数 +"ORDER\s+BY\s+[\$\#\{\+]" +"ORDER\s+BY\s+\"\s*\+" +"ORDER\s+BY\s*\"\s*\.\s*\$" + +# FROM 句にユーザ入力 +"FROM\s+\"\s*\.\s*\$" +"FROM\s+\$\w+" +"FROM\s+\"\s*\+\s*\w+" +``` + +### 7.4 自動チェックのレビューチェックリスト + +#### SQL 構築箇所の検出 + +- [ ] `SELECT`/`INSERT`/`UPDATE`/`DELETE` を含む文字列リテラルがあるか +- [ ] その文字列に変数連結(`+`/`.`/`&`/`#{}`/`$var`)があるか +- [ ] その変数がユーザ入力(リクエストパラメータ・Cookie・ヘッダ等)由来か + +#### プレースホルダ使用の確認 + +- [ ] `prepare()` / `PreparedStatement` / `SqlCommand` 等を使用しているか +- [ ] プレースホルダ位置に変数連結していないか +- [ ] `setString`/`setInt`/`bind_param` 等で型を明示しているか +- [ ] MySQL Connector/J の場合、`useServerPrepStmts=true` が設定されているか +- [ ] Perl + MySQL の場合、`mysql_server_prepare=1` が設定されているか + +#### 識別子の動的組み立て + +- [ ] テーブル名・カラム名・ソート順を変数化している箇所があるか +- [ ] その箇所でホワイトリスト検証を行っているか + +#### 文字エンコーディング + +- [ ] DB 接続文字列で `characterEncoding=sjis`/`ujis` 等の指定がないか +- [ ] PHP の `php.ini` 内部エンコーディングが Shift_JIS になっていないか +- [ ] 接続エンコーディングと内部エンコーディングが一致しているか +- [ ] DB 全体(Oracle: `AL32UTF8`、MySQL/PostgreSQL: UTF-8)が Unicode で統一されているか + +#### quote/エスケープ関数の使用 + +- [ ] `addslashes`、`htmlspecialchars` を SQL エスケープとして使っていないか +- [ ] 数値リテラル箇所で文字列用 quote/エスケープのみ使用していないか +- [ ] Perl の `DBD::mysql` で `quote($v, SQL_INTEGER)` を使っていないか + +### 7.5 検出シグネチャ優先度 + +| 優先度 | パターン | 理由 | +|---|---|---| +| Critical | SQL 文字列リテラル + ユーザ入力変数の連結 | 典型的な SQL インジェクション | +| Critical | 識別子(`ORDER BY` 等)へのユーザ入力直接埋め込み | プレースホルダで防げない | +| High | `Statement.executeQuery/Update` に動的 SQL | `PreparedStatement` を使うべき | +| High | MySQL Connector/J で `characterEncoding=sjis` または未指定 + 動的プレースホルダ | JVN#59748723 | +| High | Perl DBD::mysql で `$dbh->quote($v, SQL_INTEGER)` | 数値検証されない | +| Medium | `addslashes`、`htmlspecialchars` を SQL エスケープに使用 | DB 連動でない・安全でない | +| Medium | `prepare` 使用だが SQL 自体に文字列連結 | プレースホルダの意味がない | +| Low | エンコーディング不一致 | 副次的な脆弱性の温床 | +| Low | DB アカウントの過剰権限 | 影響範囲拡大要因 | + +### 7.6 False Positive を避けるための補助判定 + +- 文字列リテラル中の `'` がペアになっているか確認(プレースホルダ `?` か検証) +- 連結される変数が定数 / リテラル / 関数の戻り値の場合は無視できる +- ORM/クエリビルダの内部実装(Rails の `Arel`、Django の `QuerySet` 等)は無視する +- テストコード / マイグレーション / シード等は除外可 +- 環境変数や設定ファイル値(DB 接続文字列のホスト名等)は影響を受けない + +--- + +## 関連ルール ID(safe_sql_details 専用補足) + +- IPA-SSC-SAFESQL-001: 動的プレースホルダ + characterEncoding=sjis/ujis (JVN#59748723) +- IPA-SSC-SAFESQL-002: Perl DBD::mysql で quote($v, SQL_INTEGER) の使用 +- IPA-SSC-SAFESQL-003: PHP MDB2 で型指定なしの prepare +- IPA-SSC-SAFESQL-004: Perl で mysql_server_prepare=0 (動的のみ) +- IPA-SSC-SAFESQL-005: Java MySQL で useServerPrepStmts=true 未指定 +- IPA-SSC-SAFESQL-006: 識別子(テーブル/カラム/ORDER BY)への入力直接埋め込み +- IPA-SSC-SAFESQL-007: addslashes / htmlspecialchars を SQL エスケープに使用 +- IPA-SSC-SAFESQL-008: 数値リテラルへの文字列用エスケープのみ +- IPA-SSC-SAFESQL-009: prepare 内 SQL 自体への文字列連結 +- IPA-SSC-SAFESQL-010: DB 接続で Shift_JIS / EUC-JP 指定 + +## 参考 + +- IPA「安全な SQL の呼び出し方」 PDF: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017320.pdf +- IPA「安全なウェブサイトの作り方」: https://www.ipa.go.jp/security/vuln/websecurity.html +- JVN#59748723: MySQL Connector/J における SQL インジェクション脆弱性: http://jvn.jp/jp/JVN59748723/index.html +- マイクロソフト MSDN「Unicode データの使用」: http://msdn.microsoft.com/ja-jp/library/ms191200.aspx diff --git a/skills/ipa-security-check/knowledge/web_health_check.md b/skills/ipa-security-check/knowledge/web_health_check.md new file mode 100644 index 0000000..c14cefb --- /dev/null +++ b/skills/ipa-security-check/knowledge/web_health_check.md @@ -0,0 +1,535 @@ +--- +name: web_health_check +ipa_document: ウェブ健康診断仕様(「安全なウェブサイトの作り方」別冊) +ipa_section: "全章(13診断項目)" +ipa_page: "1-30" +ipa_url: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf +cwe: 複数(各項目参照) +--- + +# ウェブ健康診断仕様(IPA 別冊) - 静的解析可能項目の整理 + +## 出典 + +- 文書名: ウェブ健康診断仕様(「安全なウェブサイトの作り方」別冊) +- 版: 第1版 第1刷 / 30ページ / 2012年12月26日 +- 発行者: 独立行政法人 情報処理推進機構(IPA) +- 編集責任: 小林 偉昭 +- 原典: 財団法人地方自治情報センター(LASDEC)「ウェブ健康診断事業」(平成20年度版/平成22年度版) +- PDF URL: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf +- 関連 URL: https://www.ipa.go.jp/security/vuln/websecurity.html + +## 位置づけと注意事項 + +- ウェブサイトを人間に例えるなら「健康診断」にあたる**簡素な脆弱性診断** +- **必要かつ最小限の診断項目・検査パターンに絞り込んだ**簡素な診断 +- 抜き取り調査(全ページ診断ではない) +- 検出されなかった場合でも、**脆弱性が存在する可能性は残る** +- 検出された場合でも、誤検出の可能性 +- 診断時にウェブサイトが停止したり、意図しないデータが登録される可能性 +- インフラ管理者の許可を事前に得る + +### 危険度基準 + +| 危険度 | 説明 | +|---|---| +| 高 | 能動的脆弱性。大量の情報漏洩や改ざんの被害可能性 | +| 中 | 受動的脆弱性、または被害規模が限定的な能動的脆弱性 | +| 低 | 攻撃成功率が低い、または被害軽微 | + +### 総合判定基準 + +| 所見 | 基準 | +|---|---| +| 要治療・精密検査 | 危険度「高」または「中」の脆弱性が検出 | +| 差し支えない | 危険度「低」のみ検出((E)(J)(K)(M) のみ) | +| 異常は検出されなかった | すべて正常/該当なし | + +--- + +## 13 診断項目一覧 + +| 項番 | 記号 | 診断項目 | 危険度 | 攻撃種別 | 静的解析カバー度 | +|---|---|---|---|---|---| +| 1 | (A) | SQL インジェクション | 高 | 能動的 | ◎ | +| 2 | (B) | クロスサイト・スクリプティング | 中 | 受動的 | ◎ | +| 3 | (C) | CSRF | 中 | 受動的 | ○ | +| 4 | (D) | OS コマンド・インジェクション | 高 | 能動的 | ◎ | +| 5 | (E) | ディレクトリ・リスティング | 低〜高 | 能動的 | ○ | +| 6 | (F) | メールヘッダ・インジェクション | 中 | 能動的 | ◎ | +| 7 | (G) | パストラバーサル | 高 | 能動的 | ◎ | +| 8 | (H) | 意図しないリダイレクト | 中 | 受動的 | ◎ | +| 9 | (I) | HTTP ヘッダ・インジェクション | 中 | 受動的 | ○ | +| 10 | (J) | 認証 | 低〜中 | 能動的 | ○ | +| 11 | (K) | セッション管理の不備 | 低〜高 | 能動的/受動的 | ○ | +| 12 | (L) | 認可制御の不備、欠落 | 高 | 能動的 | △ | +| 13 | (M) | クローラへの耐性 | 低〜中 | 能動的 | △ | + +凡例: ◎=高い精度で検出可能、○=ある程度検出可能、△=コンテキスト依存 + +--- + +## 診断対象画面(機能)の定義 + +| 画面名 | コード上の対応 | +|---|---| +| ログイン | 認証用ハンドラ、`/login` 等 | +| ログアウト | `/logout` ハンドラ、セッション破棄処理 | +| パスワード変更 | パスワード更新エンドポイント | +| 入力内容確認 | 入力値をエスケープせずに再表示するテンプレート | +| DB 更新 | INSERT/UPDATE/DELETE を発行するハンドラ | +| DB アクセス | SELECT クエリを発行するハンドラ | +| エラー | エラーハンドラ、404/500 ページ | +| ファイル名 | `file=`, `xxxxfile`, `filexxxx` 等を扱うハンドラ | +| Cookie | レスポンスで Cookie を設定するコード | +| リダイレクト | redirect 処理、`location.href` 設定 | +| メール送信 | SMTP 送信処理、`mail()`, `SmtpClient` 等 | +| 認可制御有 | 権限チェックを伴うハンドラ | + +--- + +## 1. (A) SQL インジェクション【静的解析カバー: ◎】 + +**危険度**: 高 / **攻撃種別**: 能動的 / **対象画面**: DB アクセス + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | `'`(シングルクォート1つ) | エラーになる | +| 2 | `検索キー` と `検索キー'and'a'='a` の比較 | 同じ結果になる | +| 3 | `検索キー(数値)` と `検索キー and 1=1` の比較 | 同じ結果になる | + +DBMS エラーメッセージ判定基準: 製品名表示、SQL の一部表示、構文エラー指摘、英語の異質メッセージ。 + +### 静的解析できるパターン + +詳細は `01_sql_injection.md` / `safe_sql_details.md` 参照。 + +### 動的検査でないと困難 + +- 実行時に組み立てられる動的 SQL(テーブル名や ORDER BY カラムが入力依存) +- ORM の `raw()` `execute()` 経由でのインジェクション +- ストアドプロシージャ内の動的 SQL + +--- + +## 2. (B) クロスサイト・スクリプティング (XSS)【静的解析カバー: ◎】 + +**危険度**: 中 / **攻撃種別**: 受動的 / **対象画面**: 入力内容確認、エラー + +### 動的検査の検出パターン + +| # | 注入文字列 | 判定 | +|---|---|---| +| 1 | `'>"
` | エスケープされず出力 | +| 2 | `'>"` | エスケープされず出力 | +| 3 | `` (URL 中のファイル名部分) | エスケープされず出力 | +| 4 | `javascript:alert(document.cookie);` | href 属性等に出力 | + +### 静的解析できるパターン + +詳細は `05_xss.md` 参照。 + +### 動的検査でないと困難 + +- DOM ベース XSS(HTTP レスポンスに現れず JS で発火) +- 複数ステップの reflection(DB 保存 → 別画面で出力) +- mXSS(mutation XSS) + +--- + +## 3. (C) CSRF【静的解析カバー: ○】 + +**危険度**: 中 / **攻撃種別**: 受動的 / **対象画面**: パスワード変更、DB 更新、メール送信 + +### 動的検査の検出パターン + +| # | 判定基準 | +|---|---| +| 1 | トークン等のパラメータが存在しない / 削除しても処理が実行される / トークン文字列の推測が可能 / 別ユーザのトークンが使用できる | + +### 静的解析できるパターン + +詳細は `06_csrf.md` 参照。 + +### 動的検査でないと困難 + +- トークン暗号強度の評価 +- フレームワーク設定ミス(`csrf_exempt` の濫用)の実行時条件 + +--- + +## 4. (D) OS コマンド・インジェクション【静的解析カバー: ◎】 + +**危険度**: 高 / **攻撃種別**: 能動的 / **対象画面**: ファイル名、メール送信 + +### 動的検査の検出パターン + +時間ベースのブラインドインジェクション: + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | (UNIX) `\|../../../../../../../bin/sleep 20\|` | 20秒遅延 | +| 2 | (UNIX) `;/bin/sleep 20` | 20秒遅延 | +| 3 | (Windows) `\|../../../../../../../windows/system32/ping –n 21 127.0.0.1\|` | 20秒遅延 | +| 4 | (Windows) `&/windows/system32/ping –n 21 127.0.0.1` | 20秒遅延 | + +### 静的解析できるパターン + +詳細は `02_os_command_injection.md` 参照。 + +--- + +## 5. (E) ディレクトリ・リスティング【静的解析カバー: ○ 設定ファイル】 + +**危険度**: 低〜高 / **攻撃種別**: 能動的 / **対象画面**: 任意 + +### 動的検査の検出パターン + +URL の末尾のファイル名部分を削除して HTTP リクエストを送り、ファイル一覧がレスポンスされるか確認。 + +### NG パターン(設定ファイル) + +- Apache: `Options +Indexes` または `Options Indexes` +- Nginx: `autoindex on;` +- IIS: `` +- Express: `serve-index` ミドルウェアの使用 +- Tomcat `web.xml`: DefaultServlet の `listings` パラメータが `true` + +### 検出正規表現 + +``` +# Apache +^[^#]*Options\s+[^#\n]*\bIndexes\b +# Nginx +^[^#]*autoindex\s+on +# Express +require\(['"]serve-index['"]\) +# Tomcat web.xml +listings\s*true +``` + +### OK パターン + +- Apache: `Options -Indexes` を明示 +- Nginx: `autoindex` ディレクティブなし(デフォルト off) +- 各ディレクトリに空の `index.html` を配置 +- バックアップファイル(`*.bak`, `*~`, `*.swp`, `*.old`)を Web 公開ディレクトリに置かない + +--- + +## 6. (F) メールヘッダ・インジェクション【静的解析カバー: ◎】 + +**危険度**: 中 / **攻撃種別**: 能動的 / **対象画面**: メール送信 + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | Subject/From/To 欄に `%0d%0aTo:XXX@example.jp` | 新たな宛先指定可能 | +| 2 | Subject/From/To 欄に `%0d%0a%0d%0akensa` | 本文改ざん可能 | + +### 静的解析できるパターン + +詳細は `08_mail_header_injection.md` 参照。 + +### 動的検査でないと困難 + +メールが実際に届くテスト環境がないと動的検査ができないため、**静的解析が有効**。 + +--- + +## 7. (G) パス名パラメータの未チェック/ディレクトリ・トラバーサル【静的解析カバー: ◎】 + +**危険度**: 高 / **攻撃種別**: 能動的 / **対象画面**: ファイル名 + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | (UNIX) `../../../../../../../etc/hosts` | `/etc/hosts` が表示 | +| 2 | (UNIX) `../../../../../../../etc/hosts%00` | `/etc/hosts` が表示 | +| 3 | (Windows) `../../../../../../../windows/win.ini` | `win.ini` が表示 | +| 4 | (Windows) `../../../../../../../windows/win.ini%00` | `win.ini` が表示 | + +### 静的解析できるパターン + +詳細は `03_directory_traversal.md` 参照。 + +--- + +## 8. (H) 意図しないリダイレクト(オープンリダイレクト)【静的解析カバー: ◎】 + +**危険度**: 中 / **攻撃種別**: 受動的 / **対象画面**: リダイレクト + +### 動的検査の検出パターン + +クエリストリング等の URL を別ドメイン(`http://www.example.jp/`)に変更してリクエスト送信し、Location/META Refresh/JS でその URL に遷移するか。 + +### NG パターン + +- PHP: `header("Location: " . $_GET['url'])` +- Python Flask: `redirect(request.args.get('next'))` +- Python Django: `HttpResponseRedirect(request.GET['next'])` +- Java Spring: `return "redirect:" + url;`(`url` がユーザ入力) +- Node.js Express: `res.redirect(req.query.url)` +- JavaScript: `location.href = userInput`, `location.replace(userInput)`, `window.open(userInput)` +- HTML: `` + +### 検出正規表現 + +``` +# サーバサイドリダイレクト +header\s*\(\s*["']Location:\s*["']\s*\.\s*\$_(GET|POST|REQUEST) +redirect\s*\(\s*request\.(args|GET|POST) +res\.redirect\s*\(\s*req\.(query|body|params) +# クライアントサイド +location\.(href|assign|replace)\s*=\s*\w+\.value +``` + +### OK パターン + +- リダイレクト先 URL のホワイトリスト検証(同一オリジン限定など) +- 相対パスのみ許容、絶対 URL は拒否 +- リダイレクト URL を ID で受け取り、内部マッピングで実 URL に変換 +- 中継ページで明示的に「外部サイトに遷移します」と表示 + +--- + +## 9. (I) HTTP ヘッダ・インジェクション【静的解析カバー: ○】 + +**危険度**: 中 / **攻撃種別**: 受動的 / **対象画面**: Cookie、リダイレクト + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | Cookie 値に `%0d%0aSet-Cookie:xxxtest%3Dxxxtest%3B` を入力 | レスポンスヘッダに `xxxtest=xxxtest` が出現 | +| 2 | リダイレクト URL に `%0d%0aSet-Cookie:xxxtest%3Dxxxtest%3B` を入力 | 同上 | + +### 静的解析できるパターン + +詳細は `07_http_header_injection.md` 参照。 + +--- + +## 10. (J) 認証【静的解析カバー: ○】 + +**危険度**: 低〜中 / **攻撃種別**: 能動的 / **対象画面**: ログイン、ログアウト + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | パスワード最大文字数が 8 文字以上確保されているか | 8 文字未満は脆弱 | +| 2 | パスワード文字種が数字のみ・英字のみに限定されていないか | 限定は脆弱 | +| 3 | パスワード入力時に伏字になっているか | 伏字でない場合は脆弱 | +| 4 | パスワード間違いのメッセージは適切か | ID/パスワードどちらが間違いか分かるメッセージは脆弱 | +| 5 | ログアウト機能はあるか、適切に実装されているか | ない/戻るボタンで再開可は脆弱 | +| 6 | 10 回パスワードを間違えるとロックされるか | ロックされない場合は脆弱 | + +検出パターン 1 かつ 2 が検出または 5 が検出された場合は「要治療・精密検査」。 + +### NG パターン(静的解析) + +- ``(`type="password"` を使うべき) +- パスワード長制限: 8 文字未満のバリデーション規則 +- パスワード文字種制限: 数字のみ/英字のみを許容 +- エラーメッセージ: 「ユーザ ID が存在しません」「パスワードが違います」のように分けて表示 +- ログアウト処理: `session_destroy()`, `session.invalidate()` の欠落 +- ログアウト後の `Cache-Control: no-store` ヘッダ欠落 +- アカウントロック処理がない + +### 検出正規表現 + +``` +# パスワード入力欄が type="text" +]*name=["']password["'][^>]*type=["']text["'] + +# パスワード最小長 < 8 +(minlength|min_length|min)\s*[:=]\s*[1-7][^0-9] +password.*\.length\s*[<>]\s*[1-7]\b + +# 詳細なログインエラー +"(ユーザID|user(name|_id))[^"]*(存在しない|not found|invalid)" +"パスワード[^"]*(違|間違|incorrect|invalid)" +``` + +### OK パターン + +- パスワードポリシー: 最低 8 文字、英数字混在、`type="password"` 属性 +- 共通エラーメッセージ: 「ユーザ ID または パスワードが正しくありません」 +- セッション破棄を伴うログアウト +- ログアウト後のページに `Cache-Control: no-store, no-cache, must-revalidate` +- アカウントロック機構(連続失敗時の遅延、ロック、CAPTCHA) +- パスワードのハッシュ保存(`password_hash()`, `bcrypt`, `argon2`) + +--- + +## 11. (K) セッション管理の不備【静的解析カバー: ○】 + +**危険度**: 低〜高 / **攻撃種別**: 能動的/受動的 / **対象画面**: ログイン、ログアウト + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | ログインの前後でセッション ID が変化するか | 変化しないと脆弱 | +| 2 | 言語・ミドルウェアのセッション管理機構を使用しているか | 手作りは脆弱 | +| 3 | SSL で Cookie に Secure 属性が付与されているか | 付与なしは脆弱 | +| 4 | Cookie オフ時にセッション ID が URL 埋め込みにならないか | URL 埋め込みは脆弱 | +| 5 | 携帯向け等で URL 埋め込みのセッション ID が Referer 経由で漏洩しないか | 漏洩リスクは脆弱 | + +検出パターン 2 が検出または 4 かつ 5 の場合は「要治療・精密検査」。 + +標準セッション ID 名: `PHPSESSID`, `JSESSIONID`, `ASPSESSIONIDxxxx` + +### 静的解析できるパターン + +詳細は `04_session_management.md` 参照。 + +--- + +## 12. (L) 認可制御の不備、欠落【静的解析カバー: △】 + +**危険度**: 高 / **攻撃種別**: 能動的 / **対象画面**: 認可制御有 + +### 動的検査の検出パターン + +| # | 検出パターン | 判定 | +|---|---|---| +| 1 | URL 操作により、現在のユーザでは実行権限のない機能が実行可能 | 脆弱 | +| 2 | 文書 ID、注文番号、顧客番号等を変更して権限のない情報を閲覧できるか | 脆弱 | +| 3 | hidden, Cookie に現在権限が指定されており、その変更で実行権限のない機能が実行可能 | 脆弱 | + +### 静的解析できるパターン + +詳細は `11_access_control.md` 参照。 + +--- + +## 13. (M) クローラへの耐性【静的解析カバー: △】 + +**危険度**: 低〜中 / **攻撃種別**: 能動的 / **対象画面**: 全般 + +### 動的検査の手順 + +#### ① アクセス方法 +- HTTP シリアルアクセス(1リクエスト送信→レスポンス受信が完了するまで次を送らない) +- HTTP リクエストに Cookie は付加しない + +#### ② 負荷のかけ方 +- 最大 0.5 秒に 1 回 +- HTTP レスポンス受信完了後、必ず 0.5 秒待機 + +#### ③ 診断対象ページの選定 +- トップページ URL を起点にクローリング +- A タグから同一ドメイン内の URL を抽出。サブドメイン・別ドメインは原則対象外 +- JS、Java アプレット、Flash 等に含まれる URL や、手動入力を要するフォームの遷移は行わない + +#### ④ 診断対象ページ数範囲 +- 最小 1 ページから最大 4,800 ページ + +#### ⑤ 診断方法 +- HTTP リクエスト送信後、5 秒経過してもレスポンス受信が完了しない場合は HTTP 接続を切断し 5 秒待機後に再開 +- 終了条件: 全ページ完了 / 40 分経過 / 脆弱性判定基準該当 + +#### ⑥ 脆弱性の判定基準 + +「× 異常」危険度「中」: +1. 5 秒経過レスポンス未完了が **5 回連続** +2. 上記が**累計 10 回**かつ`10 ÷ 総アクセス数 × 100` が **10% 以上** +3. 400/500 系エラーが **5 回連続** +4. 上記エラーが**累計 10 回**かつ`10 ÷ 総アクセス数 × 100` が **10% 以上** + +「× 異常」危険度「低」: +5. 5 秒未完了が累計 10 回かつ 10% 未満 +6. 400/500 系エラーが累計 10 回かつ 10% 未満 + +### NG パターン(静的解析) + +- 全件取得型のクエリ(ページネーションがない一覧画面) +- N+1 クエリ問題 +- セッション/Cookie に依存し、毎リクエストで重い処理(クローラは Cookie 非付与でアクセス) +- レスポンス末尾までストリーミングを完了させずに切断 +- アクセスログ書き込みでロックが発生する実装 + +### 検出パターン + +``` +# N+1 候補 +for\s+\w+\s+in\s+.*:\s*\n[^\n]*\.\w+\.(get|find|filter)\( + +# ページネーション欠落 +\.findAll\(\) +SELECT \* FROM \w+(?!.*LIMIT) +\.all\(\) # Django: 全件取得 + +# 重い同期処理を毎リクエスト +@app\.route.*\n.*time\.sleep +``` + +### OK パターン + +- ページネーション(LIMIT/OFFSET、Cursor pagination)の徹底 +- DB クエリの timeout 設定 +- レートリミット(429 Too Many Requests を適切に返す) +- 静的キャッシュ(CDN、`Cache-Control` ヘッダ) +- WAF / robots.txt によるクローラ誘導 +- DB のインデックス設計 + +--- + +## Skill 実装上の推奨アプローチ + +### 1. 第1層: 正規表現ベースの粗い検出 +- 各検出パターンの正規表現で対象ファイルから候補行を抽出 +- 言語別フィルタ(拡張子, シェバン)で誤検出を減らす + +### 2. 第2層: AST / セマンティック解析 +- 候補行について、入力源(`$_GET`, `req.query` 等)からシンク(SQL 実行、`echo`, `exec`)までの**汚染解析(taint analysis)** +- フレームワーク特有のサニタイズ関数を「sanitizer」として登録 + +### 3. 第3層: フレームワーク固有のルール +- Rails / Django / Spring / Express / Laravel など、各フレームワークで「OK パターン」が異なるため別ルールセットを用意 +- 設定ファイル(`settings.py`, `application.yml`, `web.xml`, `php.ini`)の検査 + +### 4. 第4層: 「健康診断」と同じく抜き取り判定 +- すべてのコードを完全には保証できないことを明示 +- 危険度(高/中/低)を IPA の基準で出力 +- 検出時の参考リンクとして「安全なウェブサイトの作り方」の該当章を提示 + +--- + +## 関連ルール ID(健康診断ベースライン) + +- IPA-WHC-A-SQLI: SQL インジェクション最低ライン検査 +- IPA-WHC-B-XSS: XSS 最低ライン検査 +- IPA-WHC-C-CSRF: CSRF 最低ライン検査 +- IPA-WHC-D-OSI: OS コマンド・インジェクション最低ライン検査 +- IPA-WHC-E-LIST: ディレクトリ・リスティング設定検査 +- IPA-WHC-F-MHI: メールヘッダ・インジェクション最低ライン検査 +- IPA-WHC-G-PATH: ディレクトリ・トラバーサル最低ライン検査 +- IPA-WHC-H-REDIRECT: オープンリダイレクト検査 +- IPA-WHC-I-HHI: HTTP ヘッダ・インジェクション最低ライン検査 +- IPA-WHC-J-AUTH: 認証機能ベースライン検査(パスワードポリシ/`type="password"`/エラーメッセージ) +- IPA-WHC-K-SESS: セッション管理ベースライン検査 +- IPA-WHC-L-AUTHZ: 認可制御最低ライン検査 +- IPA-WHC-M-CRAWLER: クローラ耐性(N+1, ページネーション等) + +## 用語対応表(LASDEC 公開 → IPA 公開) + +| LASDEC 平成22年度版 | IPA 公開版 | +|---|---| +| クロスサイト・スクリプティング(XSS) | クロスサイト・スクリプティング | +| クロスサイト・リクエスト・フォージェリ(CSRF) | CSRF(クロスサイト・リクエスト・フォージェリ) | +| メールヘッダインジェクション | メールヘッダ・インジェクション | +| パストラバーサル | パス名パラメータの未チェック/ディレクトリ・トラバーサル | +| アクセス制御 | 認可制御 | + +## 参考 + +- IPA「ウェブ健康診断仕様」(PDF): https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf +- IPA「安全なウェブサイトの作り方」: https://www.ipa.go.jp/security/vuln/websecurity.html +- 旧 LASDEC「ウェブ健康診断」: https://www.lasdec.or.jp/cms/12,1284.html (IPA へ移管済み) diff --git a/skills/ipa-security-check/lib/orchestrator.md b/skills/ipa-security-check/lib/orchestrator.md new file mode 100644 index 0000000..2ec7901 --- /dev/null +++ b/skills/ipa-security-check/lib/orchestrator.md @@ -0,0 +1,349 @@ +# Orchestrator — メイン処理ロジック + +このドキュメントはメインの Claude が `/ipa-security-check` 起動時に従う実行ロジックを定義する。 + +## 全体フロー + +``` +[Claude main] + │ + ├─ Step 1: 引数解釈 + │ └─ lib/scope_resolver.md を読んで対象ファイルを列挙 + │ + ├─ Step 2: シャーディング計画 + │ └─ lib/shard_planner.md を読んでカテゴリ x シャード単位の起動計画を作る + │ + ├─ Step 3: フェーズ別並列起動 (Agent tool) + │ ├─ Phase 1 (Critical) + │ ├─ Phase 2 (High) + │ ├─ Phase 3 (Medium) + │ └─ Phase 4 (横断系) + │ + ├─ Step 4: 結果集約 + │ └─ 各エージェントの findings[] を結合し dedupe・severity フィルタ + │ → .tmp/ipa-security-check/findings_raw.json に保存 + │ + ├─ Step 5: 偽陽性レビュー (Phase 5) + │ ├─ scripts/snippet_hash.py で findings に snippet_hash 付与 + │ │ → findings_with_hash.json + │ ├─ findings を再シャーディング (5 件/shard 固定) + │ ├─ 15-false-positive-review エージェントを並列起動 + │ └─ verdicts[] を結合 → verdicts.json に保存 + │ + ├─ Step 6: (Step 7 の scripts/render_report.py が内部で実施) + │ ├─ 出力先 Markdown が既存ならパースして prior_state を抽出 + │ └─ snippet_hash 一致で findings にステータスを引き継ぐ + │ + └─ Step 7: 出力生成 + └─ scripts/render_report.py が verdict マージ・triage マージ・ + Markdown + SARIF 出力を一括で実施 +``` + +--- + +## Step 1: 引数解釈 + +`commands/ipa-security-check.md` に定義された引数を解釈する。 + +### 例 + +``` +/ipa-security-check --categories sqli,xss --severity high src/ +``` + +を以下に分解: + +```yaml +scope: + paths: ["src/"] + diff_mode: false +filter: + categories: ["sqli", "xss"] + severity_min: "high" +output: + files: ["ipa-security-report.md", "ipa-security-report.sarif"] +``` + +詳細仕様は `lib/scope_resolver.md`。 + +--- + +## Step 2: シャーディング計画 + +`lib/shard_planner.md` に従い、対象ファイルをカテゴリ x シャード単位に分割する。 + +例 (PHP ファイル 47 件 + Java ファイル 18 件、shard size = 10): + +```yaml +shards: + - agent: 01-sql-injection + shard_id: 1 + files: [src/a.php, src/b.php, ...] # 10 件 + - agent: 01-sql-injection + shard_id: 2 + files: [...] # 10 件 + - agent: 01-sql-injection + shard_id: 3 + files: [...] # 10 件 + - agent: 01-sql-injection + shard_id: 4 + files: [...] # 10 件 + - agent: 01-sql-injection + shard_id: 5 + files: [src/k.php, ..., src/q.php] # 7 件 + - agent: 05-xss + shard_id: 1 + files: [...] + ... +``` + +--- + +## Step 3: フェーズ別並列起動 + +公式制約により**サブエージェントから二次サブエージェントは起動できない**。よって**メインから全シャードを並列起動**する。 + +### 並列度の制御 + +- 1 メッセージ内で `Agent` ツールを複数並列発行できる +- 1 フェーズあたりの**同時起動上限は 8** (デフォルト) +- 8 を超える shard がある場合は、複数フェーズに自然分割する + +### フェーズ定義 + +| フェーズ | カテゴリ | 起動するエージェント | +|---|---|---| +| Phase 1 (Critical) | SQLi / OS コマンド / トラバーサル | `01-sql-injection`, `02-os-command-injection`, `03-directory-traversal` | +| Phase 2 (High) | XSS / CSRF / セッション / HTTP ヘッダ / アクセス制御 | `05-xss`, `06-csrf`, `04-session-management`, `07-http-header-injection`, `11-access-control` | +| Phase 3 (Medium) | メールヘッダ / クリックジャッキング / BoF | `08-mail-header-injection`, `09-clickjacking`, `10-buffer-overflow` | +| Phase 4 (横断系) | SQL 深掘り / 健康診断 / 運用 | `safe-sql-details`, `web-health-check`, `operation-checklist` | + +ユーザーが `--categories` で絞った場合は該当エージェントのシャードだけ起動する。 + +### サブエージェント起動の書式 + +各シャードは以下の入力で `Agent` ツールに渡す。 + +``` +description: "-shard- 検査" +subagent_type: "general-purpose" +prompt: <下記の構造化プロンプト> +``` + +構造化プロンプト (テンプレート): + +``` +あなたは IPA Security Check Skill の「」サブエージェントです。 +必ず以下の手順で動作してください。 + +# 1. 検出ルールを読み込む +.claude/skills/ipa-security-check/rules/.yaml を読んでください。 + +# 2. IPA 原文知識を読み込む +.claude/skills/ipa-security-check/knowledge/.md を読んでください。 + +# 3. 検査対象ファイル (このシャードの担当) + + +# 4. 検査方法 +- 各ファイルを Read で開く +- rules の各パターン (regex / ast) で候補を抽出 +- 抽出箇所のコード文脈を確認し、誤検知を除く +- 確定した問題箇所のみ JSON の findings[] に積む +- インラインマーカー "ipa-skip: " が直前行にある場合はその指摘を抑制 + +# 5. 出力 +以下の JSON のみ最終出力してください。コード本文や中間ログは返さないでください。 + +{ + "agent": "", + "files_scanned": , + "findings": [ ... ], + "errors": [ ... ] +} + +findings の各要素は SKILL.md「出力契約」の形式に厳密に従うこと。 +ipa.document, ipa.section, ipa.page, ipa.url を必ず埋めること +(値は knowledge/.md と rules/.yaml に書いてある)。 +``` + +### 並列発行の例 + +メイン Claude は 1 つのメッセージ内で複数 `Agent` ツールを同時呼び出す: + +``` + 01-sql-injection-shard-1 + 01-sql-injection-shard-2 + 02-os-command-injection-shard-1 + 03-directory-traversal-shard-1 +``` + +すべて結果が揃ってから次のフェーズへ進む。 + +--- + +## Step 4: 結果集約 + +各シャードの返答 JSON を結合する。メインが直接マージし、結果は作業ファイルに保存: + +```python +all_findings = [] +for shard_result in results: + all_findings.extend(shard_result["findings"]) + +# 重複排除: (file, line, rule_id) をキーに dedupe +# severity フィルタ: filter.severity_min 以下を除外 +# 順序: severity 降順 → file 名 → line 番号昇順 +``` + +severity の序列: + +``` +critical > high > medium > low > info +``` + +マージ済み配列は `Write` ツールで `.tmp/ipa-security-check/findings_raw.json` へ書き出す。 +Step 5 以降のスクリプトはこのファイルを入力にする。 + +--- + +## Step 5: 偽陽性レビュー (Phase 5) + +Step 4 で集約された findings[] に対し、`15-false-positive-review` エージェントを並列起動して偽陽性候補を識別する。 + +### 5.1 入力準備 + +`lib/triage_state.md` の規定で **各 finding の `snippet_hash` を先に計算** する (Phase 5 でも Phase 6 でも使うため orchestrator が一度だけ計算)。 +計算ロジックは `scripts/snippet_hash.py` に実装済み。メインは以下を Bash で実行する: + +```bash +# 1. 集約済みの findings を作業ファイルに書き出す (Write ツール) +# → .tmp/ipa-security-check/findings_raw.json +# 2. snippet_hash を付与 +python3 .claude/skills/ipa-security-check/scripts/snippet_hash.py \ + .tmp/ipa-security-check/findings_raw.json \ + .tmp/ipa-security-check/findings_with_hash.json +``` + +作業ディレクトリ命名規約: 中間 JSON はリポジトリルート直下の `.tmp/ipa-security-check/` に置く (名前に `tmp` を含めること)。 + +### 5.2 シャーディング + +findings を **5 件 / shard** で分割する (file サイズではなく件数ベース、固定)。 +`shard_planner.md` のロジックは流用せず、ここは件数固定。 + +FP 判定は各 finding について周辺コード Read + 呼び出し元 Grep を行うため、1 finding あたりのトークン消費が大きい。 +シャードあたり 5 件に抑えることで: +- 各サブエージェントの context が膨らみすぎない +- 1 件の重い finding (大きいファイル / 呼び出し元が多い) が他を巻き込まない +- 並列度を稼ぎやすい (20 findings → 4 shards、25 findings → 5 shards) + +### 5.3 並列起動 + +``` +description: "false-positive-review-shard-" +subagent_type: "general-purpose" +prompt: <下記> +``` + +``` +あなたは IPA Security Check Skill の「15-false-positive-review」サブエージェントです。 +必ず .claude/skills/ipa-security-check/agents/15-false-positive-review.md を読み、 +そこに書かれた手順で動作してください。 + +# このシャードで判定する findings (件) + + + +# 手順 (要約) +- 各 finding の file を Read で開き、line ±30 行の周辺コードを確認 +- 該当箇所が関数内なら関数名を特定し、Grep で呼び出し元最大 10 件を確認 +- 代表的な呼び出し元 1〜3 件について引数の出どころを軽く確認 +- 偽陽性パターン (静的定数・厳格な検証・型強制・正しいエスケープ・テストコードなど) に該当するか判定 + +最終出力は agents/15-false-positive-review.md の「出力」セクションの JSON 形式のみ。 +コード本文や思考過程は返さないこと。 +``` + +並列度はメイン制約 (1 メッセージ 8 並列) に従う。 +例: 40 findings → 8 shards (1 サブフェーズ)、60 findings → 12 shards (8 + 4 の 2 サブフェーズ)。 + +### 5.4 verdict の集約 (ファイル保存のみ) + +各 shard が返した `verdicts[]` を 1 つの JSON 配列に結合し、作業ファイルへ保存する: + +``` +.tmp/ipa-security-check/verdicts.json +``` + +`snippet_hash` をキーに finding に書き戻す処理は **Step 7 の `scripts/render_report.py` が一括で行う** ため、メインがここで finding にマージする必要はない。 + +--- + +## Step 6: トリアージ状態マージ + +`lib/triage_state.md` の規定に従う。Step 7 の `scripts/render_report.py` が以下を自動で実行する: + +1. 出力先 Markdown が既に存在すればその内容を読み込み、コードフェンス内の例示ブロックを除外した上で `` を抽出する +2. `snippet_hash` をキーに `{status, triaged_at, triaged_by, note}` を辞書化 +3. 新 findings に対して既存トリアージを書き戻し、無いものは `未対応` で初期化 +4. ステータスに従ってセクション振り分け: + +| 条件 | 出力先 | +|---|---| +| `fp_verdict == "likely_false_positive"` **かつ** `status != "対応する"` | `## 偽陽性候補` | +| `status ∈ { "問題なし", "保留" }` | `## トリアージ済み (抑止)` | +| それ以外 (`未対応`, `対応する`) | `## 検出結果` (通常) | + +> 「対応する」と既に判定済みのものは FP 判定より優先 (ユーザー意思を尊重) + +サマリ件数は `## 検出結果` セクションに出るものだけを Critical/High/Medium/Low/Info で計上する。 +`## 偽陽性候補` `## トリアージ済み (抑止)` は参考件数として別表で示す。 + +--- + +## Step 7: 出力生成 + +`lib/output_formatter.md` と `templates/report.md.tmpl` の仕様は `scripts/render_report.py` に実装済み。メインは以下を Bash で実行する: + +```bash +python3 .claude/skills/ipa-security-check/scripts/render_report.py \ + .tmp/ipa-security-check/findings_with_hash.json \ + .tmp/ipa-security-check/verdicts.json \ + ./ipa-security-report.md \ + ./ipa-security-report.sarif \ + --scope-summary "" \ + --files-scanned +``` + +このスクリプトが行う処理: +1. verdict を `snippet_hash` で findings にマージ (Step 5.4 相当) +2. 出力先 Markdown が存在すれば triage を引き継ぎ (Step 6 相当) +3. 3 セクション振り分け / サマリ集計 / `templates/report.md.tmpl` 置換 / SARIF 生成 + +`--output` で別パスが指定されていれば、第 3・第 4 引数をそのパスに差し替える。 +最後にユーザーへ要約を 1〜2 文で報告する (件数とファイル名)。 + +--- + +## エラーハンドリング + +- シャードが空 (対象ファイル 0 件) のカテゴリは起動しない +- シャードがエラーを返したら `errors[]` に記録し、他のシャードの結果は採用する +- 全シャード失敗時のみコマンド失敗とする + +--- + +## 進捗報告 + +各フェーズの開始時にユーザーへ 1 行報告する。 + +``` +Phase 1 (Critical): 5 shards 起動中… +Phase 1 完了: 12 件検出 +Phase 2 (High): 8 shards 起動中… +... +Phase 5 (偽陽性レビュー): 3 shards 起動中… 46 件中 4 件を偽陽性候補と判定 +Phase 6 (トリアージ状態マージ): 既存 18 件中 5 件のステータスを引き継ぎ +``` diff --git a/skills/ipa-security-check/lib/output_formatter.md b/skills/ipa-security-check/lib/output_formatter.md new file mode 100644 index 0000000..a52f27d --- /dev/null +++ b/skills/ipa-security-check/lib/output_formatter.md @@ -0,0 +1,214 @@ +# Output Formatter — Markdown / SARIF 生成 + +集約済み findings[] (snippet_hash / fp_verdict / status などのトリアージ属性を含む) を Markdown レポートと SARIF 2.1.0 に変換する。 + +## 入力 + +```yaml +findings: [...] # snippet_hash, fp_verdict, fp_confidence, fp_reason, + # status, triaged_at, triaged_by, note を含む +files_scanned: 247 +errors: [...] +output_files: + - ipa-security-report.md + - ipa-security-report.sarif +scope: + paths: ["src/"] + diff_mode: false +filter: + categories: [...] + severity_min: "low" +``` + +## findings の振り分け + +`lib/triage_state.md` Step 6.3 の規則で 3 セクションに振り分ける。 + +```python +detect_section = [] # 通常の検出結果 +fp_section = [] # 偽陽性候補 +triaged_section= [] # トリアージ済み (抑止) + +for f in findings: + if f.fp_verdict == "likely_false_positive" and f.status != "対応する": + fp_section.append(f) + elif f.status in ("問題なし", "保留"): + triaged_section.append(f) + else: + detect_section.append(f) +``` + +サマリ件数は `detect_section` のみで計上する。 + +## Markdown 生成 + +`templates/report.md.tmpl` を読み、以下の変数を埋め込む。 + +| 変数 | 値 | +|---|---| +| `{{scan_date}}` | 実行日時 ISO 8601 | +| `{{scope_summary}}` | スコープ要約 (パス/差分モード) | +| `{{files_scanned}}` | スキャンファイル数 | +| `{{total_findings}}` | detect_section の件数 | +| `{{count_critical}}` 〜 `{{count_info}}` | severity 別件数 (detect_section のみ) | +| `{{count_fp}}` | fp_section の件数 | +| `{{count_not_an_issue}}` | triaged_section のうち `問題なし` | +| `{{count_deferred}}` | triaged_section のうち `保留` | +| `{{findings_by_category}}` | detect_section をカテゴリ別にブロック化 | +| `{{findings_false_positive}}` | fp_section をブロック化 | +| `{{findings_triaged}}` | triaged_section をブロック化 | +| `{{errors_section}}` | エラー一覧 (あれば) | + +### finding ブロックの形式 (全セクション共通) + +すべての finding ブロックは **triage ブロック (HTML コメント) を直下に必ず置く**。 +これがないと次回スキャンでトリアージが引き継げない。 + +```markdown +### [CRITICAL] IPA-SWS-1-SQLI-001 — 文字列連結によるSQL組み立て + + + +**Status**: {{status}} + +- **File**: `src/users.php:45:12` +- **Category**: SQL Injection (CWE-89) +- **IPA**: [安全なウェブサイトの作り方 改訂第7版 1.1 SQLインジェクション (p.6-12)](https://www.ipa.go.jp/security/vuln/websecurity/about.html) +- **Remediation Type**: 根本的解決 +- **Remediation**: プレースホルダによる SQL 文の組み立て + +**問題箇所**: +\`\`\`php +$sql = "SELECT * FROM users WHERE id = " . $_GET['id']; +\`\`\` + +**修正例**: +\`\`\`php +$stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); +$stmt->execute([':id' => $_GET['id']]); +\`\`\` +``` + +#### 偽陽性候補セクション固有の追加表記 + +`{{findings_false_positive}}` に出すブロックは、上記に加え以下を追加する: + +```markdown +**FP 判定**: likely_false_positive (confidence: medium) +**FP 理由**: 直前で htmlspecialchars($v, ENT_QUOTES | ENT_HTML5, 'UTF-8') が適用済み。検出ルールは関数名のみで第2引数の有無を見ていない +``` + +#### トリアージ済みセクション固有の追加表記 + +`{{findings_triaged}}` に出すブロックは追加表記なし (status と note で十分)。 + +### グルーピング順 + +- detect_section: severity 降順 → category 昇順 → file 昇順 → line 昇順 +- fp_section / triaged_section: category 昇順 → file 昇順 → line 昇順 + +## SARIF 生成 + +`templates/sarif.json.tmpl` を読み、SARIF 2.1.0 仕様に変換。 + +### 出力対象 + +- `detect_section` の finding を `results[]` に展開 (通常) +- `triaged_section` の finding は `results[].suppressions[]` を埋めた上で `results[]` に含める (SARIF 標準の抑止表現) +- `fp_section` の finding も `results[].suppressions[]` (`kind: "external"`, `justification` に FP 理由) を埋めて出す + +SARIF コンシューマ (GitHub Code Scanning など) は `suppressions` を尊重して非表示にする。 + +### マッピング + +| finding フィールド | SARIF フィールド | +|---|---| +| `rule_id` | `results[].ruleId` | +| `severity` | `results[].level` (critical/high → error, medium → warning, low/info → note) | +| `message` | `results[].message.text` | +| `file` | `results[].locations[0].physicalLocation.artifactLocation.uri` | +| `line` | `results[].locations[0].physicalLocation.region.startLine` | +| `column` | `results[].locations[0].physicalLocation.region.startColumn` | +| `code_snippet` | `results[].locations[0].physicalLocation.region.snippet.text` | +| `snippet_hash` | `results[].partialFingerprints.snippetHash` | +| `status` | `results[].properties.triageStatus` | +| `note` | `results[].properties.triageNote` | +| `triaged_at` | `results[].properties.triagedAt` | +| `triaged_by` | `results[].properties.triagedBy` | +| `fp_verdict` | `results[].properties.fpVerdict` | +| `fp_reason` | `results[].properties.fpReason` | +| `ipa.url` | `tool.driver.rules[].helpUri` | +| `ipa.document + section + page` | `tool.driver.rules[].help.markdown` | +| `cwe` | `tool.driver.rules[].properties.tags[]` | +| `remediation` | `tool.driver.rules[].help.text` | + +### suppressions の埋め方 + +```json +"suppressions": [ + { + "kind": "external", + "status": "accepted", // status=問題なし のときは accepted, 保留 のときは underReview + "justification": "" + } +] +``` + +| 元のステータス / FP | SARIF kind | SARIF status | +|---|---|---| +| status = 問題なし | `external` | `accepted` | +| status = 保留 | `external` | `underReview` | +| fp_verdict = likely_false_positive | `external` | `underReview` | + +### tool.driver 情報 + +```json +{ + "driver": { + "name": "IPA Security Check Skill", + "version": "1.0.0", + "informationUri": "https://www.ipa.go.jp/security/vuln/websecurity/about.html", + "rules": [...] + } +} +``` + +### rules[] の生成 + +`detect_section` + `triaged_section` + `fp_section` のすべてで使われた `rule_id` をユニーク抽出し、`rules/` 配下の YAML から定義を引いて `rules[]` を構築。 + +## 書き出し + +```bash +# Markdown +Write(output.markdown_path, rendered_markdown) + +# SARIF +Write(output.sarif_path, rendered_sarif) +``` + +`--output` で指定があれば該当パスへ書く。 + +## ユーザーへの最終報告 + +``` +✅ IPA セキュリティチェック完了 + +スキャン: 247 ファイル / 14 カテゴリ +検出 (要対応): 18 件 (Critical 2 / High 6 / Medium 7 / Low 3) +偽陽性候補: 4 件 (本文外) +トリアージ済み: 5 件 (問題なし 3 / 保留 2) + +出力: ipa-security-report.md + ipa-security-report.sarif + +要対応の Critical: +- src/users.php:45 IPA-SWS-1-SQLI-001 (SQL Injection) +- src/admin.php:88 IPA-SWS-1-OSCMD-002 (OS Command Injection) +``` diff --git a/skills/ipa-security-check/lib/scope_resolver.md b/skills/ipa-security-check/lib/scope_resolver.md new file mode 100644 index 0000000..2990e29 --- /dev/null +++ b/skills/ipa-security-check/lib/scope_resolver.md @@ -0,0 +1,126 @@ +# Scope Resolver — 対象ファイル列挙ロジック + +`/ipa-security-check` の引数を解釈し、検査対象ファイル一覧を作る。 + +## 入力 + +```yaml +paths: ["src/"] # 引数で渡されたパス/glob 配列。空なら ["."] +diff_mode: false # --diff が指定されたか +categories: [...] # --categories で絞られたカテゴリ +``` + +## 出力 + +```yaml +files: + - path: src/users.php + language: php + size_bytes: 4231 + - path: src/lib/db.java + language: java + size_bytes: 2980 + ... +``` + +## アルゴリズム + +### 1. パス展開 + +- `paths` が空なら `["."]` を使う +- 各要素がディレクトリなら再帰的に走査 +- 各要素が glob なら shell の `find` または Read で展開 +- 各要素がファイルなら単独で対象 + +`--diff` の場合は以下に置き換え: + +```bash +git diff --name-only main...HEAD -- '*.php' '*.java' '*.rb' '*.py' '*.js' '*.jsx' '*.ts' '*.tsx' '*.vue' '*.cs' '*.go' '*.conf' '*.xml' '*.yaml' '*.yml' '*.htaccess' '*.cshtml' '*.aspx' 'Dockerfile' 'nginx.conf' +``` + +### 2. 除外パターン + +以下は常に除外: + +``` +node_modules/ +vendor/ +.git/ +.next/ +.nuxt/ +dist/ +build/ +out/ +target/ +__pycache__/ +.venv/ +venv/ +*.min.js +*.min.css +*.map +**/test/** # ※ ユーザー指定があれば外す +**/tests/** +**/__tests__/** +**/spec/** +``` + +`.gitignore` も考慮 (`git check-ignore -v` で確認)。 + +### 3. 言語判定 + +| 拡張子 | language | +|---|---| +| `.php` | php | +| `.java`, `.jsp` | java | +| `.rb`, `.erb` | ruby | +| `.py` | python | +| `.js`, `.jsx` | javascript | +| `.ts`, `.tsx`, `.vue` | typescript | +| `.cs`, `.cshtml`, `.aspx` | csharp | +| `.go` | go | +| `.conf`, `.htaccess`, `nginx.conf` | webserver-config | +| `web.xml` | java-webapp-config | +| `*.yaml`, `*.yml` | yaml | +| `Dockerfile` | dockerfile | + +判定不能な拡張子はスキップ。 + +### 4. ファイルサイズフィルタ + +- 1 ファイル 500KB 超は除外 (生成物の可能性が高い) +- 0 バイトは除外 + +### 5. カテゴリ別マッピング + +`--categories` で絞られている場合、関係ないファイル種別を除外: + +| カテゴリ | 対象言語 | +|---|---| +| sqli, oscmd, traversal, xss, csrf, http-header, mail-header, bof, access-control, safe-sql | php, java, ruby, python, javascript, typescript, csharp, go | +| session | php, java, ruby, python, javascript, typescript, csharp, go (+ 設定ファイル) | +| clickjacking, ops, whc | すべて (特に webserver-config, dockerfile, yaml) | + +## 実装メモ + +Bash で `find` を使う場合の例: + +```bash +find -type f \ + \( -name '*.php' -o -name '*.java' -o -name '*.jsp' -o \ + -name '*.rb' -o -name '*.erb' -o -name '*.py' -o \ + -name '*.js' -o -name '*.jsx' -o -name '*.ts' -o \ + -name '*.tsx' -o -name '*.vue' -o -name '*.cs' -o \ + -name '*.cshtml' -o -name '*.aspx' -o -name '*.go' -o \ + -name '*.conf' -o -name '*.yaml' -o -name '*.yml' -o \ + -name '*.xml' -o -name 'Dockerfile' -o -name '*.htaccess' \) \ + -not -path '*/node_modules/*' \ + -not -path '*/vendor/*' \ + -not -path '*/.git/*' \ + -not -path '*/dist/*' \ + -not -path '*/build/*' \ + -not -name '*.min.js' \ + -not -name '*.min.css' \ + -size -500k +``` + +ファイル数を数えた上で、`lib/shard_planner.md` に渡す。 diff --git a/skills/ipa-security-check/lib/shard_planner.md b/skills/ipa-security-check/lib/shard_planner.md new file mode 100644 index 0000000..818c6b4 --- /dev/null +++ b/skills/ipa-security-check/lib/shard_planner.md @@ -0,0 +1,104 @@ +# Shard Planner — シャーディング設計 + +サブエージェントは二次サブエージェントを起動できない (公式制約) ため、対象ファイル数が多いカテゴリはメイン側で事前にシャーディングする。 + +## 入力 + +`lib/scope_resolver.md` の出力 (ファイル一覧 + 言語) + `--categories` フィルタ。 + +## 出力 + +```yaml +shards: + - agent: 01-sql-injection + shard_id: 1 + files: [src/a.php, src/b.php, ...] # 最大 shard_size 件 + rule_file: rules/sql_injection.yaml + knowledge_file: knowledge/01_sql_injection.md + - agent: 01-sql-injection + shard_id: 2 + files: [...] + - agent: 05-xss + shard_id: 1 + files: [...] + ... +``` + +## アルゴリズム + +### 1. カテゴリごとに対象ファイルをフィルタ + +各サブエージェントの担当言語マッピング: + +| エージェント | 担当言語 | rule_file | knowledge_file | +|---|---|---|---| +| 01-sql-injection | php, java, ruby, python, javascript, typescript, csharp, go | rules/sql_injection.yaml | knowledge/01_sql_injection.md | +| 02-os-command-injection | 同上 | rules/os_command_injection.yaml | knowledge/02_os_command_injection.md | +| 03-directory-traversal | 同上 | rules/directory_traversal.yaml | knowledge/03_directory_traversal.md | +| 04-session-management | 同上 + webserver-config | rules/session_management.yaml | knowledge/04_session_management.md | +| 05-xss | php, java, ruby, python, javascript, typescript, csharp, go (テンプレート含む) | rules/xss.yaml | knowledge/05_xss.md | +| 06-csrf | 同上 | rules/csrf.yaml | knowledge/06_csrf.md | +| 07-http-header-injection | 同上 | rules/http_header_injection.yaml | knowledge/07_http_header_injection.md | +| 08-mail-header-injection | 同上 | rules/mail_header_injection.yaml | knowledge/08_mail_header_injection.md | +| 09-clickjacking | php, java, ruby, python, javascript, typescript, csharp, go, webserver-config | rules/clickjacking.yaml | knowledge/09_clickjacking.md | +| 10-buffer-overflow | c, cpp, go (一部) | rules/buffer_overflow.yaml | knowledge/10_buffer_overflow.md | +| 11-access-control | php, java, ruby, python, javascript, typescript, csharp, go | rules/access_control.yaml | knowledge/11_access_control.md | +| safe-sql-details | php, java, ruby, python, javascript, typescript, csharp, go | rules/safe_sql_details.yaml | knowledge/safe_sql_details.md | +| web-health-check | すべて | rules/web_health_check.yaml | knowledge/web_health_check.md | +| operation-checklist | すべて (特に config 系) | rules/operation_checklist.yaml | knowledge/operation_checklist.md | + +### 2. シャードサイズの決定 + +デフォルト `shard_size = 10` ファイル/shard。 + +ただし以下で動的調整: + +| 条件 | shard_size | +|---|---| +| ファイル平均サイズ < 5KB | 20 | +| ファイル平均サイズ 5-50KB | 10 | +| ファイル平均サイズ > 50KB | 5 | + +1 カテゴリ合計が `shard_size` 以下なら 1 shard。 + +### 3. シャード生成 + +```python +def make_shards(category_files, shard_size): + shards = [] + for i in range(0, len(category_files), shard_size): + shards.append({ + "agent": category.agent_name, + "shard_id": (i // shard_size) + 1, + "files": category_files[i:i+shard_size], + "rule_file": category.rule_file, + "knowledge_file": category.knowledge_file, + }) + return shards +``` + +### 4. 並列度の制限 + +- 1 メッセージ内の Agent ツール並列発行上限: **8 並列** +- カテゴリ shard 合計 > 8 の場合は複数フェーズに自然分割 + +例: SQLi 5 shards + OS コマンド 3 shards + トラバーサル 2 shards = 10 shards +→ Phase 1 を 2 サブフェーズに分割 (前半 8 並列、後半 2 並列) + +## 想定例 + +20 ファイルの小さいリポジトリ: +``` +Phase 1: 01-sql-injection × 2 shards + 02-os-command-injection × 1 + 03-directory-traversal × 1 = 4 並列 +``` + +200 ファイルの大きいリポジトリ: +``` +Phase 1 (SQLi 20 shards) を 3 サブフェーズに分割 (8 + 8 + 4) +``` + +## 制約 + +- ファイル数 0 のカテゴリは shard 化しない +- `--categories` で対象外のカテゴリも shard 化しない +- shard_id は 1 始まり diff --git a/skills/ipa-security-check/lib/triage_state.md b/skills/ipa-security-check/lib/triage_state.md new file mode 100644 index 0000000..c2a9b4b --- /dev/null +++ b/skills/ipa-security-check/lib/triage_state.md @@ -0,0 +1,166 @@ +## Triage State — トリアージ状態の保持と引き継ぎ + +トリアージ状態は **Markdown レポート本体に埋め込んで保持** する (専用の状態ファイルは作らない)。 +これにより `git` 管理・PR レビュー・人間の編集がすべて Markdown 上で完結する。 + +## 4 ステータス + +| ステータス | 意味 | 次回スキャンでの扱い | +|---|---|---| +| `未対応` | 未着手。デフォルト | メインの検出結果セクションに表示 | +| `対応する` | 修正を予定 / 実施中 | メインの検出結果セクションに表示 (バッジで識別) | +| `問題なし` | 確認の上、本物の脆弱性ではない / 受容可能なリスク | `## トリアージ済み (抑止)` セクションへ移動。サマリ件数からも除外 | +| `保留` | 一旦保留。後で再判断 | `## トリアージ済み (抑止)` セクションへ移動。サマリ件数からも除外 | + +新規 finding はすべて `未対応` で生成する。 + +## snippet_hash の計算 + +トリアージ済みかどうかの同定キー。同じファイル・同じルール・同じコード断片であれば、行番号が変動しても引き継げる。 + +### 計算式 + +``` +key = rule_id + "\n" + file_normalized + "\n" + code_normalized +hash = sha256(key) の先頭 16 文字 (hex) に "sha256:" を冠したもの +``` + +### file の正規化 + +- リポジトリルートからの相対パス +- 区切り文字は `/` に統一 (Windows でも) + +### code_snippet の正規化 + +1. 改行を `\n` に統一 +2. 前後の空白行を除去 +3. 各行末の空白を除去 +4. 連続する空白 (スペース/タブ) を 1 つに圧縮 +5. 行頭インデントは保持しない (圧縮対象) +6. 大文字小文字は保持 (識別子の比較に効くため) + +> 行番号は **キーに含めない**。コード断片そのものが識別子。 + +## Markdown 内の Triage ブロック形式 + +各 finding 直下に **HTML コメントの triage ブロック** を埋め込む。ユーザーはこの中の `status:` と `note:` を直接編集する。 + +```markdown +#### [CRITICAL] IPA-SWS-1-SQLI-001 — 文字列連結による SQL 組み立て + + + +**Status**: 未対応 + +- **File**: `src/users.php:45` +- **IPA**: ... +``` + +### 編集可能なフィールド (ユーザーが手で変える) + +- `status` … 4 値のいずれか +- `note` … 自由記述 (理由・関連 PR 番号など)。複数行可。 + +### 編集不可なフィールド (orchestrator が機械的に書き込む) + +- `snippet_hash` … 改変するとマッチング失敗するので変えない +- `triaged_at` … ステータスが変化した最後の日時 (ISO 8601)。ユーザー編集を検知したらタイムスタンプを更新 +- `triaged_by` … 編集者名 (オプション、なくても良い) + +## 既存レポートの読み込み + +orchestrator は新レポートを書き出す **前** に、出力先と同じパス (デフォルト `./ipa-security-report.md`) を読み込む。 + +### 抽出ロジック + +1. ファイルが存在しなければ空 dict を返す (初回スキャン) +2. ファイル全体に対し以下の正規表現で triage ブロックを全件抽出 + +```regex + +``` + +3. 各ブロックの中身を YAML 風に行単位で `key: value` パース +4. `snippet_hash` をキーに dict 化 + +```python +{ + "sha256:1a2b3c4d5e6f7890": { + "status": "問題なし", + "triaged_at": "2026-05-10T12:34:00Z", + "triaged_by": "alice", + "note": "テストフィクスチャ用のダミー値で外部入力なし" + }, + ... +} +``` + +### 不正ブロックの扱い + +- `status` が 4 値以外 → 警告ログを出し `未対応` 扱い +- `snippet_hash` が `sha256:` で始まらない → スキップ +- パース失敗 → スキップして `errors[]` に記録 + +## 新規 findings との突き合わせ + +新しい findings に対し: + +```python +for f in new_findings: + f["snippet_hash"] = compute_snippet_hash(f) + prior = prior_state.get(f["snippet_hash"]) + if prior is None: + f["status"] = "未対応" + f["triaged_at"] = "-" + f["triaged_by"] = "-" + f["note"] = "-" + else: + f["status"] = prior["status"] + f["triaged_at"] = prior["triaged_at"] + f["triaged_by"] = prior["triaged_by"] + f["note"] = prior["note"] +``` + +## 出力先の振り分け + +`status` の値で出力セクションを決める: + +| status | 出力先セクション | サマリへの計上 | +|---|---|---| +| `未対応` | `## 検出結果` (通常) | する | +| `対応する` | `## 検出結果` (通常、バッジ付き) | する | +| `問題なし` | `## トリアージ済み (抑止)` | しない | +| `保留` | `## トリアージ済み (抑止)` | しない | + +加えて、`偽陽性 review` で `likely_false_positive` と判定された finding は **status に関係なく** `## 偽陽性候補` セクションへ移動する (検出結果セクションには出さない)。 +ただし `likely_false_positive` でかつ既存トリアージで `対応する` になっているものは「ユーザーが対応すると明示した」ことを優先し、通常の `## 検出結果` に残す (誤判定耐性)。 + +## 既トリアージとの完全一致抑止 + +ユーザー要件: 「既にトリアージしているものと**全く同じ内容**の結果は抑止」 + +- `snippet_hash` 一致 + `status ∈ { 問題なし, 保留 }` → 通常レポートから抑止 (`## トリアージ済み (抑止)` に移す) +- `snippet_hash` 一致 + `status ∈ { 未対応, 対応する }` → 通常レポートに残す (まだトリアージが終わっていないため) + +## ユーザーへの説明 (レポート末尾に常に記載) + +`templates/report.md.tmpl` の脚注として以下を必ず出す: + +```markdown +## トリアージの使い方 + +- 各 finding 直下の `` ブロックの `status:` を編集してください +- 利用できる値: `未対応` / `対応する` / `問題なし` / `保留` +- `note:` には判断理由を自由に書けます (複数行可) +- 次回 `/ipa-security-check` 実行時、同じ snippet_hash の finding にステータスが引き継がれます +- `問題なし` / `保留` にした finding は次回以降「トリアージ済み (抑止)」セクションへ移動し、本文の検出結果やサマリから外れます +- `snippet_hash` 行は触らないでください (一致判定が壊れます) +``` diff --git a/skills/ipa-security-check/rules/access_control.yaml b/skills/ipa-security-check/rules/access_control.yaml new file mode 100644 index 0000000..b6ab995 --- /dev/null +++ b/skills/ipa-security-check/rules/access_control.yaml @@ -0,0 +1,221 @@ +category: access_control +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.11 アクセス制御や認可制御の欠落" + url: https://www.ipa.go.jp/security/vuln/websecurity/access-control.html +cwe: + - CWE-284 + - CWE-285 + - CWE-639 + - CWE-862 + - CWE-863 + +rules: + - id: IPA-SWS-1-ACL-001 + title: "外部入力をそのままSQL検索キーに (IDOR)" + severity: critical + remediation_type: 根本的解決 + remediation: "user_id はセッションから取得し、WHERE id = ? AND user_id = ? で絞り込む" + languages: + php: + patterns: + - type: regex + regex: 'WHERE\s+id\s*=\s*[\$''"]?\$_(GET|POST|REQUEST)' + description: "WHERE id = $_GET... の直渡し" + - type: regex + regex: 'WHERE\s+user_id\s*=\s*\$_(GET|POST|REQUEST)' + description: "WHERE user_id = $_GET... の直渡し" + - type: regex + regex: '(user_id|userId)\s*=\s*\$_(GET|POST|REQUEST)\[' + description: "user_id をリクエストパラメータから取得" + ruby: + patterns: + - type: regex + regex: '\bfind\s*\(\s*params\[:id\]\s*\)' + description: "Model.find(params[:id]) で current_user 経由なし" + python: + patterns: + - type: regex + regex: '\w+\.objects\.get\s*\(\s*id\s*=\s*request\.(GET|POST)' + description: "Model.objects.get に request.GET/POST 直渡し" + javascript: + patterns: + - type: regex + regex: '(user_id|userId)\s*=\s*req\.(query|body|params)\.' + description: "user_id を req.query/body から取得" + fix_example: + php: | + $current_user_id = $_SESSION['user_id']; + $order = db_query("SELECT * FROM orders WHERE id = ? AND user_id = ?", + [$order_id, $current_user_id]); + + - id: IPA-SWS-1-ACL-002 + title: "認証ガード未付与のコントローラ" + severity: high + remediation_type: 根本的解決 + remediation: "ApplicationController に before_action / @login_required / [Authorize]" + languages: + python: + patterns: + - type: regex + regex: '@(Get|Post|Put|Delete)Mapping' + description: "マッピングアノテーション (認証ガード確認要)" + ruby: + patterns: + - type: regex + regex: 'class\s+\w+Controller\s*<\s*ApplicationController' + description: "Controller 定義 (before_action 確認要)" + csharp: + patterns: + - type: regex + regex: '\[ApiController\]' + description: "ApiController 属性 ([Authorize] の有無確認要)" + + - id: IPA-SWS-1-ACL-003 + title: "認可の明示的無効化" + severity: critical + remediation_type: 根本的解決 + remediation: "重要パスで認可を外さない" + languages: + python: + patterns: + - type: regex + regex: '@csrf_exempt\b' + description: "Django @csrf_exempt" + - type: regex + regex: '\.AllowAnonymous' + description: "AllowAnonymous (.NET)" + java: + patterns: + - type: regex + regex: 'permitAll\s*\(\s*\)' + description: "Spring Security permitAll()" + csharp: + patterns: + - type: regex + regex: '\[AllowAnonymous\]' + description: "[AllowAnonymous] 属性" + ruby: + patterns: + - type: regex + regex: 'skip_before_action\s*:authenticate' + description: "skip_before_action :authenticate" + + - id: IPA-SWS-1-ACL-004 + title: "クライアント信頼パターン (Cookie / hidden の権限)" + severity: critical + remediation_type: 根本的解決 + remediation: "ロール情報はサーバ側セッションまたは検証済み JWT から取得" + languages: + php: + patterns: + - type: regex + regex: '\$_(COOKIE|POST|GET)\[[''"](role|admin|is_admin|user_type|permission|level)[''"]\]' + description: "権限情報をクライアント送信値から取得" + javascript: + patterns: + - type: regex + regex: 'request\.cookies\[[''"](role|admin|is_admin)[''"]\]' + description: "権限情報を Cookie から取得" + - type: regex + regex: '(role|is_admin|isAdmin)\s*===?\s*[''"]admin[''"]' + description: "クライアント側のみでロール判定 (サーバ側も必要)" + html: + patterns: + - type: regex + regex: ']*type=["'']hidden["''][^>]*name=["''](role|admin|is_admin|permission)["'']' + description: "hidden で権限情報を伝達" + + - id: IPA-SWS-1-ACL-005 + title: "Mass Assignment (role / is_admin への一括代入)" + severity: critical + remediation_type: 根本的解決 + remediation: "$fillable / strong_parameters / DTO で特権フィールドを除外" + languages: + php: + patterns: + - type: regex + regex: '(User|Account|Admin)::create\s*\(\s*request\(?\)?->all\(' + description: "Laravel Mass Assignment (request->all())" + - type: regex + regex: '(User|Account)::update\s*\(\s*request\(?\)?->all\(' + description: "update に request->all() 直渡し" + ruby: + patterns: + - type: regex + regex: '\.update\s*\(\s*params\[:\w+\]\s*\)' + description: "update に params 直渡し (strong_parameters 確認要)" + - type: regex + regex: '\.create!?\s*\(\s*params\[:\w+\]\s*\)' + description: "create に params 直渡し (strong_parameters 確認要)" + python: + patterns: + - type: regex + regex: 'User\.objects\.create\s*\(\s*\*\*request\.' + description: "User.objects.create(**request...) (Mass Assignment)" + + - id: IPA-SWS-1-ACL-006 + title: "JWT 検証なしの使用" + severity: critical + remediation_type: 根本的解決 + remediation: "JWT は秘密鍵で署名検証してから利用" + languages: + python: + patterns: + - type: regex + regex: 'jwt\.decode\s*\([^)]*verify\s*=\s*False' + description: "jwt.decode(verify=False)" + - type: regex + regex: 'jwt\.decode\s*\([^,)]+\s*\)' + description: "jwt.decode の第2引数 (鍵) 省略の可能性" + javascript: + patterns: + - type: regex + regex: 'jwt\.decode\s*\([^)]*\)\s*(?!\.verify)' + description: "jwt.decode のみ (verify を呼んでいない可能性)" + + - id: IPA-SWS-1-ACL-007 + title: "認可成功時 vs 認可失敗時のレスポンス差異" + severity: low + remediation_type: 保険的対策 + remediation: "404 と 403 の区別を慎重に。ID 列挙を許さない" + languages: + all: + patterns: + - type: regex + regex: 'http_response_code\s*\(\s*404\s*\)' + description: "404 返却 (認可失敗との混同チェック)" + + - id: IPA-SWS-1-ACL-008 + title: "メールアドレスのみのログイン (識別符号不該当の可能性)" + severity: medium + remediation_type: 根本的解決 + remediation: "パスワード等の秘密情報を必ず要求 (11-(i))" + languages: + all: + patterns: + - type: regex + regex: 'login\s*\(\s*[''"][^''"]*email' + description: "ログイン関数名 (パスワード要求の有無を確認)" + + - id: IPA-SWS-1-ACL-009 + title: "管理者専用エンドポイントの権限ロール未検証" + severity: critical + remediation_type: 根本的解決 + remediation: "/admin 系に hasRole('ADMIN') / [Authorize(Roles=\"Admin\")] を付与" + languages: + java: + patterns: + - type: regex + regex: '@RequestMapping\s*\([^)]*[''"]/admin' + description: "/admin 系マッピング (権限チェック確認要)" + python: + patterns: + - type: regex + regex: '@app\.route\s*\(\s*[''"]/admin' + description: "Flask /admin ルート (権限チェック確認要)" + ruby: + patterns: + - type: regex + regex: 'namespace\s+:admin' + description: "Rails admin 名前空間 (権限チェック確認要)" diff --git a/skills/ipa-security-check/rules/buffer_overflow.yaml b/skills/ipa-security-check/rules/buffer_overflow.yaml new file mode 100644 index 0000000..97b28ef --- /dev/null +++ b/skills/ipa-security-check/rules/buffer_overflow.yaml @@ -0,0 +1,182 @@ +category: buffer_overflow +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.10 バッファオーバーフロー" + url: https://www.ipa.go.jp/security/vuln/websecurity/bach-overflow.html +cwe: + - CWE-119 + - CWE-120 + - CWE-787 + +rules: + - id: IPA-SWS-1-BOF-001 + title: "危険関数 strcpy / strcat" + severity: critical + remediation_type: 根本的解決 + remediation: "strncpy + NUL 終端 / strlcpy / strcpy_s を使用" + languages: + c: + patterns: + - type: regex + regex: '\bstrcpy\s*\(' + description: "strcpy 使用 (バッファ長チェックなし)" + - type: regex + regex: '\bstrcat\s*\(' + description: "strcat 使用 (バッファ長チェックなし)" + - type: regex + regex: '\b_mbscpy\s*\(' + description: "_mbscpy 使用" + - type: regex + regex: '\bwcscpy\s*\(' + description: "wcscpy 使用" + - type: regex + regex: '\bwcscat\s*\(' + description: "wcscat 使用" + cpp: + patterns: + - type: regex + regex: '\bstrcpy\s*\(' + description: "strcpy 使用" + + - id: IPA-SWS-1-BOF-002 + title: "危険関数 sprintf / vsprintf" + severity: critical + remediation_type: 根本的解決 + remediation: "snprintf / vsnprintf を使用しサイズ指定" + languages: + c: + patterns: + - type: regex + regex: '\bsprintf\s*\(' + description: "sprintf 使用 (サイズ未指定)" + - type: regex + regex: '\bvsprintf\s*\(' + description: "vsprintf 使用 (サイズ未指定)" + - type: regex + regex: '\bswprintf\s*\(' + description: "swprintf 使用" + cpp: + patterns: + - type: regex + regex: '\bsprintf\s*\(' + description: "sprintf 使用" + + - id: IPA-SWS-1-BOF-003 + title: "危険関数 gets / scanf(\"%s\")" + severity: critical + remediation_type: 根本的解決 + remediation: "fgets またはサイズ幅指定の scanf を使用" + languages: + c: + patterns: + - type: regex + regex: '\bgets\s*\(' + description: "gets 使用 (C11 で削除済)" + - type: regex + regex: '\bscanf\s*\(\s*"[^"]*%s' + description: "scanf(\"%s\") の幅指定なし" + - type: regex + regex: '\bfscanf\s*\(\s*[^,]+,\s*"[^"]*%s' + description: "fscanf(\"%s\") の幅指定なし" + + - id: IPA-SWS-1-BOF-004 + title: "フォーマット文字列攻撃" + severity: critical + remediation_type: 根本的解決 + remediation: "第1引数を必ず固定文字列にする (printf(\"%s\", user_input))" + languages: + c: + patterns: + - type: regex + regex: '\b(printf|fprintf|sprintf|snprintf|syslog)\s*\(\s*[a-zA-Z_]\w*\s*\)' + description: "printf 系の第1引数が変数のみ" + - type: regex + regex: '\b(printf|fprintf|sprintf|snprintf|syslog)\s*\(\s*[a-zA-Z_]\w*\s*,(?!\s*"[^"]*%)' + description: "printf 系の第1引数が変数 (フォーマット文字列攻撃の可能性)" + + - id: IPA-SWS-1-BOF-005 + title: "memcpy / memmove サイズに外部入力" + severity: high + remediation_type: 根本的解決 + remediation: "サイズに整数オーバーフロー検査を追加、可能なら memcpy_s" + languages: + c: + patterns: + - type: regex + regex: '\bmem(cpy|move)\s*\([^,]+,[^,]+,[^)]*\b(argv|argc|input|size|len|n)\b' + description: "memcpy/memmove のサイズに外部入力候補" + cpp: + patterns: + - type: regex + regex: '\bmem(cpy|move)\s*\([^,]+,[^,]+,[^)]*\b(argv|input|size|len)\b' + description: "memcpy/memmove のサイズに外部入力候補" + + - id: IPA-SWS-1-BOF-006 + title: "alloca に外部入力 / 固定長スタックバッファ" + severity: high + remediation_type: 根本的解決 + remediation: "alloca は使用しない。スタック上の固定長バッファに外部入力を直書き込みしない" + languages: + c: + patterns: + - type: regex + regex: '\balloca\s*\(' + description: "alloca 使用" + - type: regex + regex: '\bchar\s+\w+\s*\[\s*\d+\s*\]\s*;' + description: "固定長スタックバッファ宣言 (要文脈確認)" + + - id: IPA-SWS-1-BOF-007 + title: "コンパイラ保護オプションの欠如" + severity: medium + remediation_type: 保険的対策 + remediation: "-fstack-protector-strong, -D_FORTIFY_SOURCE=2, -fPIE -pie を有効化" + languages: + makefile: + patterns: + - type: regex + regex: '-fstack-protector' + description: "-fstack-protector 設定 (OK signal)" + - type: regex + regex: '-D_FORTIFY_SOURCE' + description: "-D_FORTIFY_SOURCE 設定 (OK signal)" + - type: regex + regex: '-Wformat-security' + description: "-Wformat-security 設定 (OK signal)" + + - id: IPA-SWS-1-BOF-008 + title: "古い API getwd / realpath (固定バッファ版)" + severity: medium + remediation_type: 根本的解決 + remediation: "getcwd 使用、realpath は (path, NULL) で動的確保" + languages: + c: + patterns: + - type: regex + regex: '\bgetwd\s*\(' + description: "getwd 使用 (POSIX で廃止)" + - type: regex + regex: '\brealpath\s*\([^,]+,\s*\w+\s*\)' + description: "realpath の第2引数が固定バッファ (要サイズ確認)" + + - id: IPA-SWS-1-BOF-009 + title: "ネイティブ拡張 (FFI / Node-gyp / cffi) の存在" + severity: medium + remediation_type: 根本的解決 + remediation: "10-(i)-b。ネイティブ部分を最小化し、集中レビュー" + languages: + python: + patterns: + - type: regex + regex: 'import\s+(ctypes|cffi)\b' + description: "ctypes / cffi 利用 (ネイティブ呼び出し)" + ruby: + patterns: + - type: regex + regex: 'require\s+[''"]ffi[''"]' + description: "FFI 利用" + javascript: + patterns: + - type: regex + regex: 'node-gyp' + description: "node-gyp ビルド設定" diff --git a/skills/ipa-security-check/rules/clickjacking.yaml b/skills/ipa-security-check/rules/clickjacking.yaml new file mode 100644 index 0000000..39adcc2 --- /dev/null +++ b/skills/ipa-security-check/rules/clickjacking.yaml @@ -0,0 +1,110 @@ +category: clickjacking +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.9 クリックジャッキング" + url: https://www.ipa.go.jp/security/vuln/websecurity/clickjacking.html +cwe: CWE-1021 + +rules: + - id: IPA-SWS-1-CJK-001 + title: "X-Frame-Options ヘッダの欠落" + severity: high + remediation_type: 根本的解決 + remediation: "X-Frame-Options を DENY または SAMEORIGIN で設定 (9-(i)-a)" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"]X-Frame-Options:\s*(DENY|SAMEORIGIN)' + description: "X-Frame-Options 設定あり (OK signal)" + java: + patterns: + - type: regex + regex: 'setHeader\s*\(\s*["'']X-Frame-Options["'']' + description: "setHeader で X-Frame-Options (OK signal)" + - type: regex + regex: '\.frameOptions\s*\(\s*\)' + description: "Spring Security frameOptions() (OK signal)" + javascript: + patterns: + - type: regex + regex: 'helmet\.frameguard\s*\(' + description: "helmet.frameguard 利用 (OK signal)" + + - id: IPA-SWS-1-CJK-002 + title: "X-Frame-Options に危険値" + severity: high + remediation_type: 根本的解決 + remediation: "ALLOWALL や ワイルドカードは使用しない" + languages: + all: + patterns: + - type: regex + regex: 'X-Frame-Options:\s*ALLOWALL' + description: "X-Frame-Options: ALLOWALL (仕様外、無効)" + - type: regex + regex: 'X-Frame-Options:\s*ALLOW-FROM\s+\*' + description: "ALLOW-FROM * (ワイルドカード)" + + - id: IPA-SWS-1-CJK-003 + title: "CSP frame-ancestors の不在 / 危険値" + severity: medium + remediation_type: 根本的解決 + remediation: "frame-ancestors 'none' / 'self' を CSP に追加" + languages: + all: + patterns: + - type: regex + regex: 'Content-Security-Policy:[^\n]*frame-ancestors' + description: "CSP frame-ancestors 設定あり (OK signal)" + - type: regex + regex: 'frame-ancestors\s+[^;]*\*' + description: "frame-ancestors にワイルドカード *" + + - id: IPA-SWS-1-CJK-004 + title: "Apache httpd で X-Frame-Options 未設定" + severity: medium + remediation_type: 根本的解決 + remediation: "Header always set X-Frame-Options \"SAMEORIGIN\" を追加" + languages: + apache: + patterns: + - type: regex + regex: 'Header\s+(always\s+)?set\s+X-Frame-Options' + description: "Apache で X-Frame-Options 設定 (OK signal)" + + - id: IPA-SWS-1-CJK-005 + title: "Nginx で X-Frame-Options 未設定" + severity: medium + remediation_type: 根本的解決 + remediation: "add_header X-Frame-Options \"SAMEORIGIN\" always; を追加" + languages: + nginx: + patterns: + - type: regex + regex: 'add_header\s+X-Frame-Options' + description: "Nginx X-Frame-Options 設定 (OK signal)" + + - id: IPA-SWS-1-CJK-006 + title: "IIS web.config に X-Frame-Options 未設定" + severity: medium + remediation_type: 根本的解決 + remediation: " を追加" + languages: + xml: + patterns: + - type: regex + regex: ']*action=["''][^"'']*(delete|withdraw|change_password|public_setting)' + description: "重要操作フォーム (確認ステップ/再認証の有無確認)" diff --git a/skills/ipa-security-check/rules/csrf.yaml b/skills/ipa-security-check/rules/csrf.yaml new file mode 100644 index 0000000..075c91a --- /dev/null +++ b/skills/ipa-security-check/rules/csrf.yaml @@ -0,0 +1,173 @@ +category: csrf +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.6 CSRF" + url: https://www.ipa.go.jp/security/vuln/websecurity/csrf.html +cwe: CWE-352 + +rules: + - id: IPA-SWS-1-CSRF-001 + title: "GET メソッドで状態変更処理" + severity: high + remediation_type: 根本的解決 + remediation: "状態変更は POST / PUT / DELETE のみで受け付ける" + languages: + javascript: + patterns: + - type: regex + regex: 'app\.get\s*\([^)]*(delete|remove|update|transfer|withdraw|change|logout|destroy)' + description: "Express の GET ルートで状態変更系の名前" + php: + patterns: + - type: regex + regex: 'Route::get\s*\([^)]*(delete|update|transfer|change|destroy)' + description: "Laravel Route::get で状態変更系" + java: + patterns: + - type: regex + regex: '@GetMapping\s*\([^)]*(delete|update|transfer|change)' + description: "Spring @GetMapping で状態変更系" + html: + patterns: + - type: regex + regex: ']*method=["'']get["'']' + description: "form の method が GET" + + - id: IPA-SWS-1-CSRF-002 + title: "フレームワーク CSRF 保護の無効化" + severity: critical + remediation_type: 根本的解決 + remediation: "CSRF 保護は本番で有効にする" + languages: + python: + patterns: + - type: regex + regex: '@csrf_exempt\b' + description: "Django @csrf_exempt デコレータ" + - type: regex + regex: 'CSRF_ENABLED\s*=\s*False' + description: "Flask-WTF CSRF_ENABLED = False" + ruby: + patterns: + - type: regex + regex: 'skip_before_action\s*:verify_authenticity_token' + description: "Rails CSRF 検証スキップ" + - type: regex + regex: 'protect_from_forgery\s+with:\s*:null_session' + description: "Rails null_session (CSRF 機能無効化に近い)" + java: + patterns: + - type: regex + regex: '\.csrf\s*\(\s*\)\s*\.disable\s*\(\s*\)' + description: "Spring Security http.csrf().disable()" + csharp: + patterns: + - type: regex + regex: '\[IgnoreAntiforgeryToken\]' + description: "ASP.NET [IgnoreAntiforgeryToken]" + - type: regex + regex: 'ValidateAntiForgeryToken\s*=\s*false' + description: "ValidateAntiForgeryToken=false" + php: + patterns: + - type: regex + regex: 'VerifyCsrfToken[^}]*\$except\s*=\s*\[' + description: "Laravel VerifyCsrfToken の $except に追加" + + - id: IPA-SWS-1-CSRF-003 + title: "推測可能な CSRF トークン" + severity: high + remediation_type: 根本的解決 + remediation: "CSPRNG (random_bytes / SecureRandom) でトークン生成" + languages: + php: + patterns: + - type: regex + regex: '(csrf|token)[^=]*=\s*md5\s*\(' + description: "CSRF トークンを md5 で生成" + - type: regex + regex: '(csrf|token)[^=]*=\s*uniqid\s*\(' + description: "CSRF トークンを uniqid で生成" + - type: regex + regex: '(csrf|token)[^=]*=\s*time\s*\(\s*\)' + description: "CSRF トークンを time() で生成" + - type: regex + regex: 'md5\s*\(\s*\$user(_id)?\b' + description: "ユーザIDのmd5をトークン化" + fix_example: + php: | + $token = bin2hex(random_bytes(32)); + $_SESSION['csrf_token'] = $token; + + - id: IPA-SWS-1-CSRF-004 + title: "timing-safe でないトークン比較" + severity: medium + remediation_type: 根本的解決 + remediation: "hash_equals / MessageDigest.isEqual を使用" + languages: + php: + patterns: + - type: regex + regex: 'csrf_token[^;]*===?\s*\$_(POST|GET|REQUEST)' + description: "== / === でトークン比較 (hash_equals 推奨)" + java: + patterns: + - type: regex + regex: '\.equals\s*\([^)]*csrf' + description: "Java .equals() でトークン比較 (MessageDigest.isEqual 推奨)" + + - id: IPA-SWS-1-CSRF-005 + title: "POST フォームにトークン不在" + severity: high + remediation_type: 根本的解決 + remediation: " を埋め込み (6-(i)-a)" + languages: + html: + patterns: + - type: regex + regex: ']*method=["'']post["''][^>]*>(?![\s\S]{0,500}(csrf|_token|__RequestVerificationToken|authenticity_token|@csrf))' + description: "POST フォームに CSRF トークンの hidden が見当たらない" + + - id: IPA-SWS-1-CSRF-006 + title: "SameSite Cookie 属性未指定 (保険)" + severity: low + remediation_type: 保険的対策 + remediation: "SameSite=Lax または Strict をセッション Cookie に付与" + languages: + php: + patterns: + - type: regex + regex: 'setcookie\s*\([^)]*\)' + description: "setcookie 呼び出し (SameSite 確認要)" + - type: regex + regex: 'session\.cookie_samesite\s*=\s*[''"]?None' + description: "SameSite=None (Secure 必須)" + + - id: IPA-SWS-1-CSRF-007 + title: "Referer/Origin チェックの不在 (6-(i)-c)" + severity: medium + remediation_type: 根本的解決 + remediation: "Referer / Origin を確認し、空または異なるドメインなら拒否" + languages: + php: + patterns: + - type: regex + regex: '\$_SERVER\[[''"]HTTP_REFERER[''"]\]' + description: "HTTP_REFERER 参照 (OK signal)" + javascript: + patterns: + - type: regex + regex: 'req\.headers\.(referer|origin)\b' + description: "req.headers.referer/origin 参照 (OK signal)" + + - id: IPA-SWS-1-CSRF-008 + title: "重要操作前の再認証なし (6-(i)-b)" + severity: medium + remediation_type: 根本的解決 + remediation: "重要操作 (パスワード変更等) でパスワード再入力を要求" + languages: + html: + patterns: + - type: regex + regex: ']*action=["''][^"'']*(change_password|change_email|delete_account|withdraw)' + description: "重要操作フォームに current_password 入力欄があるか確認要" diff --git a/skills/ipa-security-check/rules/directory_traversal.yaml b/skills/ipa-security-check/rules/directory_traversal.yaml new file mode 100644 index 0000000..311085c --- /dev/null +++ b/skills/ipa-security-check/rules/directory_traversal.yaml @@ -0,0 +1,198 @@ +category: directory_traversal +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.3 パス名パラメータの未チェック/ディレクトリ・トラバーサル" + url: https://www.ipa.go.jp/security/vuln/websecurity/parameter.html +cwe: CWE-22 + +rules: + - id: IPA-SWS-1-TRAV-001 + title: "入力直渡しのファイル系API (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "basename + realpath で正規化し、ベースディレクトリ prefix を確認 (3-(i)-b)" + languages: + php: + patterns: + - type: regex + regex: '\b(fopen|file_get_contents|file|readfile|file_put_contents|copy|unlink|SplFileObject)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE|SERVER)\[' + description: "ファイル操作 API にスーパーグローバル直渡し" + - type: regex + regex: '\b(include|include_once|require|require_once)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' + description: "include/require に外部入力 (LFI)" + - type: regex + regex: '\b(fopen|file_get_contents|readfile|include|require)\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE)\[' + description: "ファイル系 API に外部入力を連結" + - type: regex + regex: '\b(fopen|file_get_contents|include)\s*\([^)]*\$_(GET|POST)\[[^\]]+\]\s*\.\s*["''](\.php|\.html|\.tpl)["'']' + description: "拡張子付与で include (それでも危険、basename 必須)" + fix_example: + php: | + $dir = '/var/www/templates/'; + $file = basename($_GET['file']); + $path = realpath($dir . $file); + if ($path !== false && strpos($path, $dir) === 0) { + $content = file_get_contents($path); + } + + - id: IPA-SWS-1-TRAV-002 + title: "入力直渡しのファイル系API (Java)" + severity: critical + remediation_type: 根本的解決 + remediation: "Paths.get + toRealPath + ベースディレクトリ確認" + languages: + java: + patterns: + - type: regex + regex: 'new\s+File\s*\(\s*request\.getParameter\(' + description: "new File にリクエストパラメータを直接渡している" + - type: regex + regex: 'new\s+FileInputStream\s*\(\s*request\.getParameter\(' + description: "FileInputStream に直接渡している" + - type: regex + regex: 'Paths\.get\s*\(\s*request\.getParameter\(' + description: "Paths.get にリクエストパラメータを直接渡している" + - type: regex + regex: 'Files\.(readAllBytes|readAllLines|newInputStream)\s*\(\s*Paths\.get\(\s*request\.' + description: "Files API への外部入力直渡し" + fix_example: + java: | + Path base = Paths.get("/var/www/templates").toRealPath(); + Path target = base.resolve(name).normalize().toRealPath(); + if (!target.startsWith(base)) { + throw new SecurityException(); + } + + - id: IPA-SWS-1-TRAV-003 + title: "入力直渡しのファイル系API (Python)" + severity: critical + remediation_type: 根本的解決 + remediation: "os.path.basename + realpath で正規化、ベースディレクトリ prefix 確認" + languages: + python: + patterns: + - type: regex + regex: '\bopen\s*\(\s*(request\.|flask\.request\.|os\.environ|sys\.argv)' + description: "open() に外部入力直渡し" + - type: regex + regex: '\bopen\s*\(\s*[^)]*request\.(args|form|values|GET|POST)\[' + description: "open() にリクエスト辞書直渡し" + - type: regex + regex: '\b(send_file|send_from_directory)\s*\([^)]*request\.' + description: "Flask send_file にリクエスト直渡し" + - type: regex + regex: '\bopen\s*\(\s*os\.path\.join\s*\([^)]*request\.' + description: "os.path.join 経由でもサニタイズ不足" + fix_example: + python: | + import os + base = '/var/www/templates' + name = os.path.basename(request.args['file']) + path = os.path.realpath(os.path.join(base, name)) + if not path.startswith(base): + abort(403) + + - id: IPA-SWS-1-TRAV-004 + title: "入力直渡しのファイル系API (Ruby / Rails)" + severity: critical + remediation_type: 根本的解決 + remediation: "File.basename + File.expand_path + ベース prefix 確認" + languages: + ruby: + patterns: + - type: regex + regex: '\bFile\.(read|open|new|delete)\s*\(\s*params\[' + description: "File.read 等に params 直渡し" + - type: regex + regex: '\bsend_file\s*\(\s*params\[' + description: "send_file に params 直渡し" + - type: regex + regex: '\brender\s+file:\s*params\[' + description: "render file: に params 直渡し (Local File Inclusion)" + fix_example: + ruby: | + base = '/var/www/templates' + name = File.basename(params[:file]) + path = File.expand_path(File.join(base, name)) + raise unless path.start_with?(base + '/') + File.read(path) + + - id: IPA-SWS-1-TRAV-005 + title: "入力直渡しのファイル系API (Node.js)" + severity: critical + remediation_type: 根本的解決 + remediation: "path.basename + path.resolve + ベース prefix 確認" + languages: + javascript: + patterns: + - type: regex + regex: '\bfs\.(readFile|readFileSync|createReadStream|writeFile|writeFileSync|unlink)\s*\(\s*(req\.(query|body|params|headers)|request\.)' + description: "fs API にリクエスト由来データを直渡し" + - type: regex + regex: '\bres\.sendFile\s*\(\s*(req\.(query|body|params)|userInput)' + description: "res.sendFile に外部入力直渡し" + - type: regex + regex: '\bpath\.join\s*\([^)]*req\.(query|body|params)' + description: "path.join 経由でもサニタイズ不足" + typescript: + patterns: + - type: regex + regex: '\bfs\.(readFile|readFileSync|createReadStream)\s*\(\s*req\.' + description: "fs API にリクエスト由来データを直渡し" + fix_example: + javascript: | + const base = '/var/www/templates'; + const safe = path.basename(req.query.file); + const target = path.resolve(base, safe); + if (!target.startsWith(base)) return res.status(403).end(); + + - id: IPA-SWS-1-TRAV-006 + title: "入力直渡しのファイル系API (.NET)" + severity: critical + remediation_type: 根本的解決 + remediation: "Path.GetFileName + Path.GetFullPath + ベース確認" + languages: + csharp: + patterns: + - type: regex + regex: 'File\.(ReadAllText|ReadAllBytes|Open|OpenRead|WriteAllText|Delete)\s*\(\s*Request\.' + description: "File API に Request 経由データ直渡し" + - type: regex + regex: 'new\s+FileStream\s*\(\s*Request\.' + description: "FileStream に Request 直渡し" + fix_example: + csharp: | + var baseDir = Path.GetFullPath(@"C:\app\templates\"); + var name = Path.GetFileName(Request.QueryString["file"]); + var path = Path.GetFullPath(Path.Combine(baseDir, name)); + if (!path.StartsWith(baseDir)) throw new SecurityException(); + + - id: IPA-SWS-1-TRAV-007 + title: "トラバーサル文字列を含む入力 (検出文字列)" + severity: high + remediation_type: 保険的対策 + remediation: "入力に ../ や %2f が含まれていたら処理中止 (3-(iii))" + languages: + all: + patterns: + - type: regex + regex: '\.\./|\.\.\\\\' + description: "../ または ..\ の生混入 (URL/入力に直接含まれる場合)" + - type: regex + regex: '%2e%2e%2f|%2e%2e/|\.\.%2f|\.\.%5c|%252e%252e%252f|%c0%ae%c0%ae/' + description: "URL エンコード形式のトラバーサル" + - type: regex + regex: '%00' + description: "NULL バイト (拡張子チェックバイパス)" + + - id: IPA-SWS-1-TRAV-008 + title: "basename / realpath / prefix 確認の不在" + severity: high + remediation_type: 根本的解決 + remediation: "固定ディレクトリ + basename + realpath + prefix 確認の組合せ実装" + languages: + all: + patterns: + - type: regex + regex: 'open\s*\([^)]+\.[''"]?(\.php|\.html|\.txt|\.tpl)[''"]?' + description: "拡張子付与のみで basename を経由していない可能性" diff --git a/skills/ipa-security-check/rules/http_header_injection.yaml b/skills/ipa-security-check/rules/http_header_injection.yaml new file mode 100644 index 0000000..9665142 --- /dev/null +++ b/skills/ipa-security-check/rules/http_header_injection.yaml @@ -0,0 +1,133 @@ +category: http_header_injection +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.7 HTTPヘッダ・インジェクション" + url: https://www.ipa.go.jp/security/vuln/websecurity/http-header.html +cwe: CWE-113 + +rules: + - id: IPA-SWS-1-HHDR-001 + title: "header() に外部入力を連結 (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "ヘッダ出力 API を使用し、入力に \r\n が含まれていれば 400 を返す (7-(i))" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"](Location|Set-Cookie|Refresh|Content-Disposition)[^''"]*[''"]\s*\.\s*\$' + description: "header() に Location/Set-Cookie 等の文字列連結" + - type: regex + regex: 'header\s*\(\s*[^)]*\$_(GET|POST|REQUEST|COOKIE)' + description: "header() にスーパーグローバルを直接渡している" + - type: regex + regex: 'setcookie\s*\(\s*[^,]+,\s*\$_(GET|POST|REQUEST|COOKIE)' + description: "setcookie 値にスーパーグローバル直渡し" + fix_example: + php: | + $url = $_GET['url']; + if (preg_match('/[\r\n]/', $url)) { http_response_code(400); exit; } + header('Location: ' . $url); + + - id: IPA-SWS-1-HHDR-002 + title: "response.setHeader/addHeader に外部入力 (Java Servlet)" + severity: critical + remediation_type: 根本的解決 + remediation: "入力に CRLF があれば 400。または sendRedirect 使用" + languages: + java: + patterns: + - type: regex + regex: 'response\.(setHeader|addHeader|addCookie)\s*\([^)]*request\.getParameter\(' + description: "setHeader/addHeader にリクエストパラメータ直渡し" + - type: regex + regex: 'response\.sendRedirect\s*\(\s*request\.getParameter\(' + description: "sendRedirect にリクエストパラメータ直渡し" + + - id: IPA-SWS-1-HHDR-003 + title: "Response.Redirect/AppendHeader に外部入力 (.NET)" + severity: critical + remediation_type: 根本的解決 + remediation: "入力検証後にリダイレクト、または許可リスト URL のみ" + languages: + csharp: + patterns: + - type: regex + regex: 'Response\.(Redirect|AppendHeader|AddHeader)\s*\(\s*Request\.' + description: "Response.Redirect/AddHeader に Request 直渡し" + + - id: IPA-SWS-1-HHDR-004 + title: "redirect_to に params 直渡し (Rails)" + severity: high + remediation_type: 根本的解決 + remediation: "ホワイトリストまたは allow_other_host: false で同一ホスト限定" + languages: + ruby: + patterns: + - type: regex + regex: 'redirect_to\s+params\[' + description: "redirect_to に params 直渡し" + - type: regex + regex: 'response\.headers\[[^\]]+\]\s*=\s*params\[' + description: "response.headers に params 直代入" + + - id: IPA-SWS-1-HHDR-005 + title: "res.setHeader/redirect に req 直渡し (Node.js)" + severity: critical + remediation_type: 根本的解決 + remediation: "入力検証 (\r\n 含む場合は 400) + ホワイトリスト URL" + languages: + javascript: + patterns: + - type: regex + regex: 'res\.(setHeader|writeHead|redirect)\s*\([^)]*req\.(query|body|params|headers)' + description: "res の setHeader/redirect に req 直渡し" + - type: regex + regex: 'res\.cookie\s*\([^,]+,\s*req\.(query|body|params|headers)' + description: "res.cookie 値に req 直渡し" + typescript: + patterns: + - type: regex + regex: 'res\.(setHeader|redirect)\s*\([^)]*req\.(query|body|params)' + description: "res の setHeader/redirect に req 直渡し" + + - id: IPA-SWS-1-HHDR-006 + title: "生の print でヘッダ書き出し (Perl/CGI)" + severity: high + remediation_type: 根本的解決 + remediation: "CGI モジュールの redirect / header API を使用、CRLF 除去" + languages: + perl: + patterns: + - type: regex + regex: 'print\s+["''][^"'']*(Location|Set-Cookie):[^"'']*\$\w+' + description: "print で Location/Set-Cookie ヘッダ + 変数" + + - id: IPA-SWS-1-HHDR-007 + title: "入力に CRLF (\\r\\n / %0d%0a) 検査なし" + severity: high + remediation_type: 保険的対策 + remediation: "ヘッダに使う入力で改行コード検査必須 (7-(ii))" + languages: + all: + patterns: + - type: regex + regex: '%0d%0a|%0D%0A|%0a%0d' + description: "入力に URL エンコード CRLF (ペイロード形跡)" + + - id: IPA-SWS-1-HHDR-008 + title: "オープンリダイレクト (Location に任意URL)" + severity: medium + remediation_type: 根本的解決 + remediation: "同一オリジン許可リストで Location 値を制限" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"]Location:\s*[''"]\s*\.\s*\$_(GET|POST|REQUEST)' + description: "Location ヘッダにスーパーグローバル直連結 (オープンリダイレクト)" + python: + patterns: + - type: regex + regex: 'redirect\s*\(\s*request\.(args|GET|POST|values)' + description: "Flask/Django redirect に外部入力" diff --git a/skills/ipa-security-check/rules/mail_header_injection.yaml b/skills/ipa-security-check/rules/mail_header_injection.yaml new file mode 100644 index 0000000..63301f0 --- /dev/null +++ b/skills/ipa-security-check/rules/mail_header_injection.yaml @@ -0,0 +1,128 @@ +category: mail_header_injection +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.8 メールヘッダ・インジェクション" + url: https://www.ipa.go.jp/security/vuln/websecurity/mail-header.html +cwe: CWE-93 + +rules: + - id: IPA-SWS-1-MHDR-001 + title: "mail() / mb_send_mail() に外部入力直渡し (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "宛先 (To) は固定。件名・差出人はヘッダに渡す前に改行除去 (8-(i)-a)" + languages: + php: + patterns: + - type: regex + regex: '\b(mail|mb_send_mail)\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE)' + description: "mail()/mb_send_mail() にスーパーグローバル" + - type: regex + regex: '\b(mail|mb_send_mail)\s*\([^)]*\.\s*\$\w+' + description: "mail()/mb_send_mail() の引数を文字列連結" + - type: regex + regex: '[''"](From|To|Cc|Bcc|Subject|Reply-To)\s*:[^''"]*[''"]\s*\.\s*\$_(GET|POST|REQUEST|COOKIE)' + description: "From/To/Subject 文字列にスーパーグローバル連結" + fix_example: + php: | + $to = 'info@example.com'; // 固定 + $from = preg_replace('/[\r\n]/', '', $_POST['email']); + if ($from !== $_POST['email']) { http_response_code(400); exit; } + $subject = preg_replace('/[\r\n]/', '', $_POST['subject']); + mb_send_mail($to, $subject, $body, "From: $from"); + + - id: IPA-SWS-1-MHDR-002 + title: "Python email/smtplib に外部入力直渡し" + severity: critical + remediation_type: 根本的解決 + remediation: "To は固定、From/Subject は改行除去後に msg['Subject'] = ..." + languages: + python: + patterns: + - type: regex + regex: 'msg\[[''"](To|From|Subject|Cc|Bcc)[''"]\]\s*=\s*request\.' + description: "EmailMessage のヘッダにリクエスト直代入" + - type: regex + regex: 'smtplib\.[A-Z]\w*\([^)]*\)\.sendmail\s*\([^)]*request\.' + description: "smtplib.sendmail に request 直渡し" + + - id: IPA-SWS-1-MHDR-003 + title: "Ruby Mail / ActionMailer に params 直渡し" + severity: high + remediation_type: 根本的解決 + remediation: "To/From/Subject は改行検証後または固定値に" + languages: + ruby: + patterns: + - type: regex + regex: '\b(to|from|cc|bcc|subject)\s+params\[' + description: "Mail DSL のヘッダに params 直渡し" + - type: regex + regex: '\bmail\s*\(\s*to:\s*params\[' + description: "mail(to: params[...]) パターン" + + - id: IPA-SWS-1-MHDR-004 + title: "Perl sendmail パイプにヘッダ + 変数埋め込み" + severity: critical + remediation_type: 根本的解決 + remediation: "ヘッダに使う変数は s/[\r\n].*//s で改行以降を削除" + languages: + perl: + patterns: + - type: regex + regex: 'print\s+\$\w+\s+["''](From|To|Subject|Cc|Bcc):[^"'']*\$\w+' + description: "sendmail ハンドル print にヘッダ + 変数" + - type: regex + regex: 'open\s*\([^,]+,\s*["'']\|.*sendmail' + description: "sendmail パイプ起動" + + - id: IPA-SWS-1-MHDR-005 + title: "Node.js nodemailer に req 直渡し" + severity: high + remediation_type: 根本的解決 + remediation: "to は固定値、from/subject は改行検証" + languages: + javascript: + patterns: + - type: regex + regex: 'transporter\.sendMail\s*\(\s*\{[^}]*(to|from|subject|cc|bcc)\s*:\s*req\.(query|body|params)' + description: "nodemailer に req 直渡し" + + - id: IPA-SWS-1-MHDR-006 + title: "HTML hidden に宛先メールアドレス (8-(ii))" + severity: high + remediation_type: 根本的解決 + remediation: "宛先はサーバ側に保持、HTML hidden で持たない" + languages: + html: + patterns: + - type: regex + regex: ']+type=["'']?hidden["'']?[^>]+name=["'']?(to|recipient|mail_to|mailto)["'']?[^>]+value=["''][^"'']+@' + description: "hidden で宛先メールアドレスを送信" + + - id: IPA-SWS-1-MHDR-007 + title: "入力に CRLF / ヘッダ名混入を検出" + severity: high + remediation_type: 保険的対策 + remediation: "ヘッダ用入力で \r\n や Bcc:/Cc: を含む文字列を拒否 (8-(iii))" + languages: + all: + patterns: + - type: regex + regex: '%0d%0a|%0a%0d|\\r\\n' + description: "入力に CRLF (URL エンコード形式)" + - type: regex + regex: '\b(Bcc|Cc|Subject|From|To|Content-Type)\s*:' + description: "入力にヘッダ名 (混入チェック)" + + - id: IPA-SWS-1-MHDR-008 + title: "メールアドレス形式の未検証" + severity: low + remediation_type: 保険的対策 + remediation: "RFC 準拠の正規表現でメールアドレスを検証" + languages: + all: + patterns: + - type: regex + regex: '\bmail\s*\([^)]*\$\w+[^)]*\)' + description: "mail 呼び出し (メールアドレス検証コードの有無を確認)" diff --git a/skills/ipa-security-check/rules/operation_checklist.yaml b/skills/ipa-security-check/rules/operation_checklist.yaml new file mode 100644 index 0000000..bcc0d51 --- /dev/null +++ b/skills/ipa-security-check/rules/operation_checklist.yaml @@ -0,0 +1,363 @@ +category: operation_checklist +ipa: + document: 安全なウェブサイトの運用管理に向けての20ヶ条 / 実装チェックリスト + section: "20条 / 第7版巻末チェックリスト" + url: https://www.ipa.go.jp/security/vuln/websecurity/sitecheck.html + +rules: + - id: IPA-OPS-1-001 + title: "機密設定ファイルの公開ディレクトリ配置 (第1条)" + severity: critical + remediation_type: 根本的解決 + remediation: ".env / config 系を public/ 配下から除外し .gitignore に登録" + languages: + all: + patterns: + - type: regex + regex: '(public|static|www|htdocs|wwwroot|docroot)/[^"''\s]*\.(env|env\.local|env\.production)' + description: "公開ディレクトリに .env が配置" + - type: regex + regex: '(public|static|www|htdocs)/[^"''\s]*(config\.php|application\.yml|database\.yml|web\.config|secrets\.json|credentials\.json)' + description: "公開ディレクトリに機密設定ファイル" + - type: regex + regex: '(public|static|www)/\.(git|svn|hg|DS_Store)' + description: "VCS ディレクトリ / DS_Store が公開ディレクトリ" + - type: regex + regex: '\.(bak|swp|orig|old)$' + description: "バックアップファイル拡張子 (公開ディレクトリにあれば NG)" + + - id: IPA-OPS-1-002 + title: "依存ライブラリの脆弱性管理 (第4条)" + severity: medium + remediation_type: 根本的解決 + remediation: "lock ファイル使用 + npm audit / pip-audit / Trivy / Dependabot" + languages: + all: + patterns: + - type: regex + regex: '^(package\.json|requirements\.txt|Gemfile|composer\.json|pom\.xml|build\.gradle|Cargo\.toml|go\.mod)$' + description: "依存定義ファイル (lock 同梱と監査ツール統合を確認)" + + - id: IPA-OPS-1-003 + title: "詳細エラーメッセージの本番返却 (第5条)" + severity: high + remediation_type: 根本的解決 + remediation: "display_errors=Off / DEBUG=False / include-stacktrace=never" + languages: + php: + patterns: + - type: regex + regex: 'display_errors\s*=\s*(On|1|true)' + description: "PHP display_errors=On" + - type: regex + regex: 'expose_php\s*=\s*(On|1|true)' + description: "PHP expose_php=On (Server ヘッダで PHP バージョン露出)" + python: + patterns: + - type: regex + regex: '\bDEBUG\s*=\s*True' + description: "Django/Flask DEBUG=True" + ruby: + patterns: + - type: regex + regex: 'consider_all_requests_local\s*=\s*true' + description: "Rails consider_all_requests_local=true" + java: + patterns: + - type: regex + regex: 'server\.error\.include-stacktrace\s*=\s*always' + description: "Spring Boot include-stacktrace=always" + + - id: IPA-OPS-1-004 + title: "機微情報のログ出力 (第6条)" + severity: high + remediation_type: 根本的解決 + remediation: "ログ出力からパスワード/トークン/カード番号を除外" + languages: + all: + patterns: + - type: regex + regex: '(logger\.(info|debug|warn|error)|console\.log)\s*\([^)]*(password|pwd|secret|token|authorization|cookie|cardNumber|cvv)' + description: "ログに機微情報" + + - id: IPA-OPS-1-005 + title: "HTTPS リダイレクト / HSTS ヘッダ (第7条)" + severity: high + remediation_type: 根本的解決 + remediation: "全面 HTTPS 化 + HSTS (max-age=31536000; includeSubDomains)" + languages: + nginx: + patterns: + - type: regex + regex: 'return\s+301\s+https://' + description: "Nginx HTTPS リダイレクト (OK signal)" + - type: regex + regex: 'add_header\s+Strict-Transport-Security' + description: "Nginx HSTS 設定 (OK signal)" + apache: + patterns: + - type: regex + regex: 'RewriteRule\s+[^]]*https://%\{HTTP_HOST\}' + description: "Apache HTTPS リダイレクト (OK signal)" + all: + patterns: + - type: regex + regex: 'Strict-Transport-Security:\s*max-age=0' + description: "HSTS max-age=0 (機能無効化)" + + - id: IPA-OPS-1-006 + title: "TLS バージョン / 弱い暗号スイート (第7条)" + severity: high + remediation_type: 根本的解決 + remediation: "TLS 1.2+ のみ。RC4/DES/3DES/MD5/NULL 暗号を含めない" + languages: + nginx: + patterns: + - type: regex + regex: 'ssl_protocols\s+[^;]*\b(SSLv2|SSLv3|TLSv1|TLSv1\.0|TLSv1\.1)\b' + description: "古い SSL/TLS バージョン許可" + - type: regex + regex: 'ssl_ciphers\s+[^;]*\b(RC4|DES|3DES|MD5|NULL)\b' + description: "弱い暗号スイート許可" + + - id: IPA-OPS-1-007 + title: "混在コンテンツ (HTTPS ページ内の http:// リソース)" + severity: medium + remediation_type: 根本的解決 + remediation: "全リソース URL を //protocol-relative または https:// に変更" + languages: + html: + patterns: + - type: regex + regex: '(src|href|action)\s*=\s*["'']http://' + description: "HTML 内に http:// で始まる URL (混在コンテンツの可能性)" + + - id: IPA-OPS-1-008 + title: "弱いパスワードハッシュ (第8条 / 2.5 パスワード)" + severity: critical + remediation_type: 根本的解決 + remediation: "bcrypt / argon2 / scrypt / PBKDF2 を使用" + languages: + php: + patterns: + - type: regex + regex: '\bmd5\s*\(\s*\$?password' + description: "パスワードを md5 でハッシュ" + - type: regex + regex: '\bsha1\s*\(\s*\$?password' + description: "パスワードを sha1 でハッシュ" + - type: regex + regex: '\bhash\s*\(\s*[''"]sha256[''"]\s*,\s*\$?password' + description: "パスワードを sha256 単体でハッシュ (ソルト/ストレッチング不足)" + python: + patterns: + - type: regex + regex: 'hashlib\.(md5|sha1|sha256)\s*\(\s*password' + description: "パスワードを hashlib で単純ハッシュ" + javascript: + patterns: + - type: regex + regex: 'crypto\.createHash\s*\(\s*[''"](md5|sha1)[''"]' + description: "Node.js md5/sha1 ハッシュ" + + - id: IPA-OPS-1-009 + title: "ハードコードされた認証情報 / シークレット (第12条)" + severity: critical + remediation_type: 根本的解決 + remediation: "環境変数または Secrets Manager から取得" + languages: + all: + patterns: + - type: regex + regex: '\bAKIA[0-9A-Z]{16}\b' + description: "AWS Access Key ID 形式" + - type: regex + regex: '(password|passwd|pwd|api[_-]?key|secret|token)\s*[:=]\s*[''"][A-Za-z0-9!@#$%^&*+/=]{8,}[''"]' + description: "ハードコードされたパスワード / API キー" + - type: regex + regex: '\bgh[pousr]_[A-Za-z0-9]{36,}\b' + description: "GitHub PAT 形式" + - type: regex + regex: '-----BEGIN (RSA )?PRIVATE KEY-----' + description: "秘密鍵が直書き" + + - id: IPA-OPS-1-010 + title: "弱いデフォルトパスワード (第11条)" + severity: critical + remediation_type: 根本的解決 + remediation: "テスト/サンプルアカウントを本番投入しない" + languages: + all: + patterns: + - type: regex + regex: '(password|pwd)\s*[:=]\s*[''"]?(admin|password|test|123456|root|guest|changeme)[''"]?' + description: "弱いデフォルトパスワード" + + - id: IPA-OPS-1-011 + title: "Dockerfile USER root のまま" + severity: medium + remediation_type: 根本的解決 + remediation: "非 root ユーザに USER を切り替える" + languages: + dockerfile: + patterns: + - type: regex + regex: '^USER\s+root\b' + description: "USER root 指示" + - type: regex + regex: '^FROM\s+[^:\s]+:latest\b' + description: ":latest タグ使用 (バージョン固定推奨)" + + - id: IPA-OPS-1-012 + title: "chmod 777 等の過剰パーミッション" + severity: high + remediation_type: 根本的解決 + remediation: "必要最小限のパーミッション (755 / 644)" + languages: + all: + patterns: + - type: regex + regex: '\bchmod\s+(-R\s+)?(777|666)\b' + description: "chmod 777 / 666" + + - id: IPA-OPS-1-013 + title: "クラウド Security Group の 0.0.0.0/0 全開放 (第15条)" + severity: critical + remediation_type: 根本的解決 + remediation: "SSH 22 / RDP 3389 / DB 3306,5432 等を 0.0.0.0/0 で開放しない" + languages: + terraform: + patterns: + - type: regex + regex: 'cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0/0"\s*\]' + description: "Terraform で 0.0.0.0/0 全開放" + - type: regex + regex: 'from_port\s*=\s*(22|3389|3306|5432)' + description: "重要ポートの SG 定義 (CIDR 確認要)" + + - id: IPA-OPS-1-014 + title: "S3 バケット公開 / 暗号化なし (第19条)" + severity: critical + remediation_type: 根本的解決 + remediation: "BlockPublicAccess + デフォルト暗号化を有効化" + languages: + terraform: + patterns: + - type: regex + regex: 'block_public_acls\s*=\s*false' + description: "S3 block_public_acls=false" + - type: regex + regex: 'restrict_public_buckets\s*=\s*false' + description: "S3 restrict_public_buckets=false" + yaml: + patterns: + - type: regex + regex: 'PubliclyAccessible:\s*true' + description: "CloudFormation で RDS PubliclyAccessible=true" + + - id: IPA-OPS-1-015 + title: "IAM Role のワイルドカード濫用" + severity: high + remediation_type: 根本的解決 + remediation: "最小権限原則。Resource: \"*\" / Action: \"*\" を避ける" + languages: + json: + patterns: + - type: regex + regex: '"(Action|Resource)"\s*:\s*"\*"' + description: "IAM ポリシーで Action/Resource ワイルドカード" + yaml: + patterns: + - type: regex + regex: '(Action|Resource):\s*[''"]?\*[''"]?\s*$' + description: "IAM ポリシーでワイルドカード" + + - id: IPA-OPS-1-016 + title: "セキュリティヘッダ群 (CSP / X-Content-Type-Options / Referrer-Policy)" + severity: medium + remediation_type: 保険的対策 + remediation: "CSP / X-Content-Type-Options: nosniff / Referrer-Policy / Permissions-Policy" + languages: + all: + patterns: + - type: regex + regex: 'X-Content-Type-Options:\s*nosniff' + description: "nosniff 設定 (OK signal)" + - type: regex + regex: 'Referrer-Policy:\s*(strict-origin-when-cross-origin|no-referrer|same-origin)' + description: "Referrer-Policy 設定 (OK signal)" + + - id: IPA-OPS-1-017 + title: "認証/認可ライブラリ未使用 (第8条)" + severity: medium + remediation_type: 根本的解決 + remediation: "bcrypt / argon2 / devise / passport / Spring Security を使用" + languages: + all: + patterns: + - type: regex + regex: '\b(bcrypt|argon2|scrypt|pbkdf2|password_hash)\b' + description: "推奨ハッシュライブラリ使用 (OK signal)" + + - id: IPA-OPS-1-018 + title: "アカウントロック / レートリミットの不在 (第8条)" + severity: medium + remediation_type: 根本的解決 + remediation: "express-rate-limit / django-axes / rack-attack / bucket4j 等" + languages: + javascript: + patterns: + - type: regex + regex: 'require\s*\(\s*[''"]express-rate-limit[''"]' + description: "express-rate-limit 利用 (OK signal)" + python: + patterns: + - type: regex + regex: '\bdjango[_-]axes\b' + description: "django-axes 利用 (OK signal)" + + - id: IPA-OPS-1-019 + title: "server_tokens / X-Powered-By 露出" + severity: low + remediation_type: 保険的対策 + remediation: "server_tokens off; / expose_php = Off" + languages: + nginx: + patterns: + - type: regex + regex: 'server_tokens\s+on' + description: "Nginx server_tokens on (バージョン露出)" + php: + patterns: + - type: regex + regex: 'expose_php\s*=\s*On' + description: "PHP expose_php=On" + + - id: IPA-CL-1-001 + title: "ファイルアップロード先での実行権限" + severity: high + remediation_type: 根本的解決 + remediation: "uploads ディレクトリで php/jsp/aspx/cgi の実行を禁止" + languages: + nginx: + patterns: + - type: regex + regex: 'location\s+[^{]*/uploads[^{]*\{' + description: "uploads ロケーション (実行禁止設定確認要)" + apache: + patterns: + - type: regex + regex: 'php_admin_flag\s+engine\s+off' + description: "Apache uploads で PHP 実行禁止 (OK signal)" + + - id: IPA-CL-1-002 + title: "WAF / IDS / IPS の不在 (第17条)" + severity: low + remediation_type: 保険的対策 + remediation: "ModSecurity + OWASP CRS / AWS WAF を導入" + languages: + nginx: + patterns: + - type: regex + regex: 'modsecurity\s+on' + description: "ModSecurity on (OK signal)" diff --git a/skills/ipa-security-check/rules/os_command_injection.yaml b/skills/ipa-security-check/rules/os_command_injection.yaml new file mode 100644 index 0000000..e1e71f8 --- /dev/null +++ b/skills/ipa-security-check/rules/os_command_injection.yaml @@ -0,0 +1,221 @@ +category: os_command_injection +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.2 OSコマンド・インジェクション" + url: https://www.ipa.go.jp/security/vuln/websecurity/os-command.html +cwe: CWE-78 + +rules: + - id: IPA-SWS-1-OSCMD-001 + title: "シェル経由のコマンド実行 (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "シェル起動関数を避け、配列引数を取る API を使用 (2-(i))" + languages: + php: + patterns: + - type: regex + regex: '\b(system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec)\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE|SERVER)' + description: "危険関数にスーパーグローバルを直接渡している" + - type: regex + regex: '\b(system|exec|passthru|shell_exec|popen|proc_open)\s*\([^)]*\.\s*\$\w+' + description: "危険関数に文字列連結を渡している" + - type: regex + regex: '`[^`]*\$_(GET|POST|REQUEST|COOKIE)\b' + description: "バッククォート演算子に外部入力を埋め込み" + - type: regex + regex: '\bescapeshell(arg|cmd)\s*\(' + description: "escapeshellarg/cmd のみへの依存 (根本対策ではない)" + fix_example: + php: | + $allowed = ['option1', 'option2']; + if (in_array($_GET['opt'], $allowed, true)) { + $out = file_get_contents("/safe/path"); + } + + - id: IPA-SWS-1-OSCMD-002 + title: "シェル経由のコマンド実行 (Python)" + severity: critical + remediation_type: 根本的解決 + remediation: "subprocess.run([cmd, arg], shell=False) で配列引数を使用" + languages: + python: + patterns: + - type: regex + regex: 'subprocess\.(call|run|Popen|check_call|check_output)\s*\([^)]*shell\s*=\s*True' + description: "subprocess に shell=True 指定" + - type: regex + regex: '\bos\.(system|popen)\s*\(' + description: "os.system / os.popen はシェル起動" + - type: regex + regex: '\bcommands\.(getoutput|getstatusoutput)\s*\(' + description: "commands モジュール (Python2 系) はシェル起動" + - type: regex + regex: 'subprocess\.[A-Za-z]+\s*\(\s*[''"][^''"]*\{[^}]*\}[^''"]*[''"]' + description: "subprocess の引数を format で組み立てている" + fix_example: + python: | + subprocess.run(["convert", filename, "out.png"], shell=False, check=True) + + - id: IPA-SWS-1-OSCMD-003 + title: "シェル経由のコマンド実行 (Node.js/JavaScript)" + severity: critical + remediation_type: 根本的解決 + remediation: "child_process.execFile / spawn で配列引数を使用" + languages: + javascript: + patterns: + - type: regex + regex: 'child_process\.(exec|execSync)\s*\(\s*[`"''][^`"'']*(\$\{|\+)' + description: "child_process.exec に文字列連結 / テンプレートリテラル" + - type: regex + regex: 'require\s*\(\s*[''"]child_process[''"]\s*\)\.(exec|execSync)\s*\(' + description: "child_process.exec の直接呼び出し" + - type: regex + regex: '(exec|execSync)\s*\([^)]*(req\.(query|body|params|headers)|request\.)' + description: "exec にリクエスト由来データを渡している" + - type: regex + regex: '\bspawn\s*\([^,)]+,[^)]*\bshell\s*:\s*true' + description: "spawn の shell:true オプション" + typescript: + patterns: + - type: regex + regex: 'child_process\.(exec|execSync)\s*\(\s*[`"''][^`"'']*(\$\{|\+)' + description: "child_process.exec に文字列連結 / テンプレートリテラル" + fix_example: + javascript: | + child_process.execFile("ls", [userInput]); + + - id: IPA-SWS-1-OSCMD-004 + title: "シェル経由のコマンド実行 (Java)" + severity: critical + remediation_type: 根本的解決 + remediation: "ProcessBuilder(List) で引数配列を使用" + languages: + java: + patterns: + - type: regex + regex: 'Runtime\.getRuntime\(\)\.exec\s*\(\s*"[^"]*"\s*\+' + description: "Runtime.exec に文字列連結を渡している" + - type: regex + regex: 'Runtime\.getRuntime\(\)\.exec\s*\(\s*[A-Za-z_]\w*\s*\)' + description: "Runtime.exec に単一文字列変数 (要確認)" + - type: regex + regex: 'new\s+ProcessBuilder\s*\(\s*"(sh|bash|cmd|cmd\.exe)"\s*,\s*"(-c|/c)"' + description: "ProcessBuilder で sh -c / cmd /c を使用 (シェル経由)" + - type: regex + regex: 'Runtime\.getRuntime\(\)\.exec\s*\(\s*"(sh|bash|cmd)\s+' + description: "exec に sh / bash / cmd 文字列を渡している" + fix_example: + java: | + ProcessBuilder pb = new ProcessBuilder("ls", userInput); + pb.start(); + + - id: IPA-SWS-1-OSCMD-005 + title: "シェル経由のコマンド実行 (Ruby)" + severity: critical + remediation_type: 根本的解決 + remediation: "system([\"cmd\", arg1]) や IO.popen([cmd, arg]) で配列を使用" + languages: + ruby: + patterns: + - type: regex + regex: '\bsystem\s*\(\s*[''"][^''"]*#\{' + description: "system に文字列補間" + - type: regex + regex: '\bIO\.popen\s*\(\s*[''"][^''"]*#\{' + description: "IO.popen に文字列補間" + - type: regex + regex: '`[^`]*#\{[^}]+\}[^`]*`' + description: "バッククォート演算子に文字列補間" + - type: regex + regex: '%x\{[^}]*#\{[^}]+\}' + description: "%x{} 構文に文字列補間" + - type: regex + regex: '\bKernel\.\s*(system|exec)\s*\(\s*[''"][^''"]*#\{' + description: "Kernel.system / exec に文字列補間" + - type: regex + regex: '\bexec\s*\(\s*[''"][^''"]*#\{' + description: "exec に文字列補間" + fix_example: + ruby: | + system("ls", params[:dir]) + IO.popen(["ls", params[:dir]]) + + - id: IPA-SWS-1-OSCMD-006 + title: "Perl open() のパイプ起動とシェル経由実行" + severity: critical + remediation_type: 根本的解決 + remediation: "sysopen() を使用、または open の list-form (open(F, '-|', cmd, args))" + languages: + perl: + patterns: + - type: regex + regex: '\bopen\s*\(\s*[^,]+,\s*["''][^"'']*\$\w+[^"'']*\|["'']' + description: "open() のパイプ起動引数に変数 (末尾 |)" + - type: regex + regex: '\bopen\s*\(\s*[^,]+,\s*["''][^"'']*\|\s*[^|"'']*\$\w+' + description: "open() のパイプ起動引数に変数 (先頭 |)" + - type: regex + regex: '\bsystem\s*\(\s*["''][^"'']*\$\w+' + description: "system に変数を含む文字列 (シェル経由)" + - type: regex + regex: '`[^`]*\$\w+[^`]*`' + description: "バッククォートに変数を含む" + - type: regex + regex: '\bqx[/{(\[][^/})\]]*\$\w+' + description: "qx// に変数 (シェル経由)" + fix_example: + perl: | + sysopen(my $fh, $filename, O_RDONLY) or die; + open(my $fh, '-|', '/usr/bin/grep', $keyword, 'file.txt'); + + - id: IPA-SWS-1-OSCMD-007 + title: ".NET Process.Start のシェル経由起動" + severity: critical + remediation_type: 根本的解決 + remediation: "ProcessStartInfo + ArgumentList を使用、UseShellExecute=false" + languages: + csharp: + patterns: + - type: regex + regex: 'Process\.Start\s*\(\s*"[^"]*"\s*\+' + description: "Process.Start に文字列連結" + - type: regex + regex: 'UseShellExecute\s*=\s*true' + description: "UseShellExecute = true 指定" + - type: regex + regex: 'Process\.Start\s*\(\s*"cmd(\.exe)?"\s*,\s*"/[ck]\s' + description: "cmd.exe /c の起動" + + - id: IPA-SWS-1-OSCMD-008 + title: "Go の exec.Command でシェル起動" + severity: critical + remediation_type: 根本的解決 + remediation: "exec.Command(name, args...) で引数を分離。シェル経由を避ける" + languages: + go: + patterns: + - type: regex + regex: 'exec\.Command\s*\(\s*"(sh|bash|cmd|cmd\.exe)"\s*,\s*"(-c|/c)"' + description: "exec.Command で sh -c / cmd /c" + - type: regex + regex: 'fmt\.Sprintf\s*\([^)]+\)[^)]*exec\.Command' + description: "コマンドを Sprintf で組み立てている" + + - id: IPA-SWS-1-OSCMD-009 + title: "入力ホワイトリスト検証なしのシェル経由呼び出し (汎用 2-(ii))" + severity: high + remediation_type: 保険的対策 + remediation: "引数に許可文字種を限定するホワイトリスト検証を追加" + languages: + php: + patterns: + - type: regex + regex: '\b(system|exec|shell_exec|passthru)\s*\([^)]+\)\s*;' + description: "シェル起動関数の呼び出し (引数検証コードの有無を確認)" + python: + patterns: + - type: regex + regex: 'subprocess\.\w+\s*\([^)]+\)' + description: "subprocess 呼び出し (検証コード有無を確認)" diff --git a/skills/ipa-security-check/rules/safe_sql_details.yaml b/skills/ipa-security-check/rules/safe_sql_details.yaml new file mode 100644 index 0000000..2a132be --- /dev/null +++ b/skills/ipa-security-check/rules/safe_sql_details.yaml @@ -0,0 +1,222 @@ +category: safe_sql_details +ipa: + document: 安全なSQLの呼び出し方 + section: "詳細検出パターン (SQL別冊)" + url: https://www.ipa.go.jp/security/vuln/websecurity/about.html +cwe: CWE-89 + +rules: + - id: IPA-SQL-2-LIKE-001 + title: "LIKE 句のメタ文字 (% / _) エスケープ漏れ" + severity: medium + remediation_type: 根本的解決 + remediation: "LIKE で使用する値は \ で % と _ をエスケープしてからプレースホルダ" + languages: + php: + patterns: + - type: regex + regex: 'LIKE\s+[''"]%\s*[''"]?\s*\.\s*\$' + description: "LIKE '%' . $var 形式 (メタ文字エスケープなし)" + - type: regex + regex: 'LIKE\s+CONCAT\s*\(\s*[''"]%[''"]?\s*,\s*\$' + description: "LIKE CONCAT('%', $var) (エスケープ確認要)" + java: + patterns: + - type: regex + regex: 'LIKE\s+"\s*%\s*"\s*\+\s*\w+' + description: "LIKE \"%\" + var (エスケープなし)" + + - id: IPA-SQL-2-QUOTE-001 + title: "PHP::PDO::quote の文脈外利用" + severity: medium + remediation_type: 根本的解決 + remediation: "PDO::quote はプレースホルダの代替ではない。bindValue を使う" + languages: + php: + patterns: + - type: regex + regex: '->quote\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)' + description: "PDO::quote にスーパーグローバル直渡し" + + - id: IPA-SQL-2-NUM-001 + title: "数値型に文字列用エスケープのみ適用" + severity: high + remediation_type: 根本的解決 + remediation: "数値型は intval / (int) キャストまたは bind_param 型指定" + languages: + php: + patterns: + - type: regex + regex: 'mysqli_real_escape_string\s*\(\s*[^)]+\)\s*[^;]*=\s*\$\w+' + description: "real_escape_string で数値処理 (id=$id でクォートなし)" + perl: + patterns: + - type: regex + regex: '->quote\s*\([^,]+,\s*SQL_INTEGER\s*\)' + description: "DBD::mysql quote(..., SQL_INTEGER) は入力をそのまま返す (IPA 5.4.3)" + + - id: IPA-SQL-2-IDENT-001 + title: "識別子 (テーブル名/カラム名) の動的構築" + severity: critical + remediation_type: 根本的解決 + remediation: "識別子はホワイトリスト方式で限定 (プレースホルダは識別子には効かない)" + languages: + php: + patterns: + - type: regex + regex: 'FROM\s+\$\w+' + description: "FROM 句に変数 (テーブル名動的指定)" + - type: regex + regex: 'SELECT\s+\$\w+\s+FROM' + description: "SELECT 句のカラム名に変数" + java: + patterns: + - type: regex + regex: 'FROM\s+"\s*\+\s*\w+' + description: "Java FROM 句に変数連結" + python: + patterns: + - type: regex + regex: 'FROM\s+\{\w+\}' + description: "Python FROM 句に format 変数" + + - id: IPA-SQL-2-ENC-001 + title: "DB 接続エンコーディングが Shift_JIS / EUC-JP" + severity: high + remediation_type: 根本的解決 + remediation: "UTF-8 で統一 (utf8mb4 推奨)" + languages: + java: + patterns: + - type: regex + regex: 'characterEncoding=(sjis|ujis|Shift_JIS|EUC-JP)' + description: "JDBC URL で SJIS / EUC-JP" + php: + patterns: + - type: regex + regex: 'default_charset\s*=\s*[''"]?(SJIS|Shift_JIS|EUC-JP)' + description: "php.ini default_charset が日本語非UTF8" + - type: regex + regex: 'set names\s+(sjis|ujis|cp932)' + description: "SET NAMES で SJIS/EUC-JP" + + - id: IPA-SQL-2-ENC-002 + title: "MySQL Connector/J で useServerPrepStmts 未指定" + severity: high + remediation_type: 根本的解決 + remediation: "useServerPrepStmts=true を付与 (静的プレースホルダ有効化)" + languages: + java: + patterns: + - type: regex + regex: 'jdbc:mysql:[^"'']+useServerPrepStmts=true' + description: "useServerPrepStmts=true (OK signal)" + - type: regex + regex: 'jdbc:mysql://[^"''\s]+' + description: "MySQL JDBC URL (useServerPrepStmts の有無を確認)" + + - id: IPA-SQL-2-PERL-001 + title: "Perl DBI で mysql_server_prepare 未指定" + severity: medium + remediation_type: 根本的解決 + remediation: "mysql_server_prepare=1 を DSN に追加し静的プレースホルダ有効化" + languages: + perl: + patterns: + - type: regex + regex: 'DBI:mysql:[^;''"]+mysql_server_prepare=1' + description: "mysql_server_prepare=1 (OK signal)" + + - id: IPA-SQL-2-PRE-001 + title: "prepare 引数自体に文字列連結" + severity: critical + remediation_type: 根本的解決 + remediation: "SQL リテラル中に変数を埋め込まず、? や :name のプレースホルダのみ使う" + languages: + php: + patterns: + - type: regex + regex: '->prepare\s*\(\s*[''"][^''"]*[''"]\s*\.\s*\$\w+' + description: "PDO::prepare 引数に変数連結" + - type: regex + regex: 'mysqli_prepare\s*\([^,]+,\s*[''"][^''"]*[''"]\s*\.\s*\$' + description: "mysqli_prepare 引数に変数連結" + java: + patterns: + - type: regex + regex: 'prepareStatement\s*\(\s*"[^"]*"\s*\+\s*\w+' + description: "prepareStatement 引数に変数連結" + + - id: IPA-SQL-2-PRE-002 + title: "bind_param での型指定漏れ (PHP mysqli)" + severity: medium + remediation_type: 根本的解決 + remediation: "bind_param の第1引数で i/s/d/b の型を明示" + languages: + php: + patterns: + - type: regex + regex: '\bbind_param\s*\(\s*[''"]s+[''"]\s*,' + description: "bind_param で全部 s 指定 (数値も s)" + + - id: IPA-SQL-2-RAW-001 + title: "ORM の raw SQL / find_by_sql" + severity: high + remediation_type: 根本的解決 + remediation: "ORM の raw 系 API でも必ずプレースホルダを使用" + languages: + ruby: + patterns: + - type: regex + regex: '\.find_by_sql\s*\(\s*[''"][^''"]*#\{' + description: "find_by_sql に文字列補間" + - type: regex + regex: 'exec_query\s*\(\s*[''"][^''"]*#\{' + description: "exec_query に文字列補間" + python: + patterns: + - type: regex + regex: '\.raw\s*\(\s*f[''"]' + description: "Django Model.objects.raw に f-string" + - type: regex + regex: 'text\s*\(\s*f[''"]' + description: "SQLAlchemy text() に f-string" + + - id: IPA-SQL-2-OUTPUT-001 + title: "出力エンコーディング不一致" + severity: low + remediation_type: 保険的対策 + remediation: "アプリ内部・DB・HTTP レスポンスのエンコーディングを UTF-8 で統一" + languages: + php: + patterns: + - type: regex + regex: 'internal_encoding\s*=\s*[''"]?(SJIS|Shift_JIS|EUC-JP)' + description: "PHP internal_encoding が非 UTF-8" + + - id: IPA-SQL-2-ESC-001 + title: "addslashes / stripslashes の使用" + severity: high + remediation_type: 根本的解決 + remediation: "addslashes は DB エンジン非連動。プレースホルダ使用" + languages: + php: + patterns: + - type: regex + regex: '\baddslashes\s*\(' + description: "addslashes 使用 (Shift_JIS 問題あり)" + - type: regex + regex: '\bstripslashes\s*\(' + description: "stripslashes 使用 (古いコードの兆候)" + + - id: IPA-SQL-2-NUMCAST-001 + title: "数値キャスト検証なし" + severity: medium + remediation_type: 根本的解決 + remediation: "数値は (int)/intval/ctype_digit で検証してから組み立て" + languages: + php: + patterns: + - type: regex + regex: 'WHERE\s+id\s*=\s*\$_(GET|POST|REQUEST)' + description: "WHERE id = $_GET... をクォートなしで使用 (数値前提)" diff --git a/skills/ipa-security-check/rules/session_management.yaml b/skills/ipa-security-check/rules/session_management.yaml new file mode 100644 index 0000000..4ee50d7 --- /dev/null +++ b/skills/ipa-security-check/rules/session_management.yaml @@ -0,0 +1,227 @@ +category: session_management +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.4 セッション管理の不備" + url: https://www.ipa.go.jp/security/vuln/websecurity/session-management.html +cwe: + - CWE-330 + - CWE-384 + - CWE-522 + - CWE-614 + +rules: + - id: IPA-SWS-1-SESS-001 + title: "自前セッションID生成 (弱い乱数)" + severity: critical + remediation_type: 根本的解決 + remediation: "フレームワーク標準セッション機構または CSPRNG (random_bytes 等) を使用 (4-(i))" + languages: + php: + patterns: + - type: regex + regex: 'md5\s*\(\s*(time\(\)|microtime|uniqid|date\()' + description: "md5(time()) 等の予測可能なシード" + - type: regex + regex: 'sha1\s*\(\s*(time\(\)|microtime|uniqid|date\()' + description: "sha1(time()) 等の予測可能なシード" + - type: regex + regex: '(session_id|sid|token)\s*=\s*\b(uniqid|rand|mt_rand|time)\s*\(' + description: "弱い乱数関数でセッションID生成" + - type: regex + regex: '\bmd5\s*\([^)]*\$user' + description: "ユーザIDをmd5でトークン化 (推測可能)" + javascript: + patterns: + - type: regex + regex: 'Math\.random\s*\(\s*\)' + description: "Math.random() はセッションID生成に不適" + python: + patterns: + - type: regex + regex: '\brandom\.(random|randint|choice|sample)\s*\(' + description: "random モジュール (CSPRNG ではない) でセッションID生成" + fix_example: + php: | + $sid = bin2hex(random_bytes(32)); + + - id: IPA-SWS-1-SESS-002 + title: "セッションIDをURLパラメータに格納 (4-(ii))" + severity: high + remediation_type: 根本的解決 + remediation: "Cookie のみで管理し、URL Rewriting を無効化" + languages: + php: + patterns: + - type: regex + regex: 'session\.use_trans_sid\s*=\s*1' + description: "PHP session.use_trans_sid=1 (URL Rewriting 有効)" + - type: regex + regex: 'session\.use_only_cookies\s*=\s*0' + description: "PHP session.use_only_cookies=0" + all: + patterns: + - type: regex + regex: '[?&](sid|sessionid|PHPSESSID|jsessionid)=[^&"''\s]+' + description: "URL に sid/sessionid 等が露出している" + - type: regex + regex: ';jsessionid=' + description: "Java JSESSIONID の URL Rewriting" + + - id: IPA-SWS-1-SESS-003 + title: "ログイン成功後のセッションID再生成漏れ (4-(iv)-a)" + severity: high + remediation_type: 根本的解決 + remediation: "認証成功直後に session_regenerate_id(true) / invalidate() / Session.Abandon() を呼ぶ" + languages: + php: + patterns: + - type: regex + regex: '\bauthenticate\s*\([^)]+\)\s*\)?\s*\{[^}]*\$_SESSION\[[^\]]+\]\s*=' + description: "authenticate 後すぐ $_SESSION 代入 (regenerate_id なし)" + java: + patterns: + - type: regex + regex: 'getSession\s*\(\s*\)\.setAttribute\s*\(\s*"(user|userId|authenticated)"' + description: "ログイン後 setAttribute 前に invalidate していない可能性" + ruby: + patterns: + - type: regex + regex: 'session\[:user_id\]\s*=\s*\w+\.id(?!.*reset_session)' + description: "session[:user_id] 代入前後で reset_session が呼ばれていない" + + - id: IPA-SWS-1-SESS-004 + title: "Cookie の Secure 属性なし (4-(iii))" + severity: high + remediation_type: 根本的解決 + remediation: "HTTPS 通信の Cookie に Secure 属性を必ず付与" + languages: + php: + patterns: + - type: regex + regex: 'session\.cookie_secure\s*=\s*(0|false|off|Off)' + description: "session.cookie_secure が無効" + - type: regex + regex: 'setcookie\s*\([^,]+,[^,]+,[^,]+,[^,]+,[^,]+,\s*false' + description: "setcookie の第7引数 (secure) が false" + - type: regex + regex: 'setcookie\s*\([^)]+\)' + description: "setcookie 呼び出し (Secure フラグ確認要)" + javascript: + patterns: + - type: regex + regex: '(res\.cookie|cookie)\s*\([^)]*secure\s*:\s*false' + description: "cookie secure:false 指定" + - type: regex + regex: 'cookie\s*:\s*\{[^}]*\}' + description: "cookie 属性ブロック (secure 確認要)" + java: + patterns: + - type: regex + regex: '\.setSecure\s*\(\s*false\s*\)' + description: "Cookie.setSecure(false)" + - type: regex + regex: 'server\.servlet\.session\.cookie\.secure\s*=\s*false' + description: "Spring Boot で cookie.secure=false" + csharp: + patterns: + - type: regex + regex: 'requireSSL\s*=\s*"false"' + description: "web.config の requireSSL=false" + fix_example: + php: | + ini_set('session.cookie_secure', 1); + setcookie('sid', $sid, ['secure'=>true,'httponly'=>true,'samesite'=>'Strict']); + + - id: IPA-SWS-1-SESS-005 + title: "Cookie の HttpOnly 属性なし" + severity: high + remediation_type: 保険的対策 + remediation: "XSS による Cookie 窃取を防ぐため HttpOnly を付与 (5-(ix))" + languages: + php: + patterns: + - type: regex + regex: 'session\.cookie_httponly\s*=\s*(0|false|off|Off)' + description: "session.cookie_httponly が無効" + - type: regex + regex: 'setcookie\s*\([^,]+,[^,]+,[^,]+,[^,]+,[^,]+,[^,]+,\s*false' + description: "setcookie 第8引数 (httponly) が false" + javascript: + patterns: + - type: regex + regex: '(res\.cookie|cookie)\s*\([^)]*httpOnly\s*:\s*false' + description: "cookie httpOnly:false 指定" + java: + patterns: + - type: regex + regex: '\s*false\s*' + description: "web.xml で http-only=false" + + - id: IPA-SWS-1-SESS-006 + title: "SameSite 属性未指定" + severity: medium + remediation_type: 保険的対策 + remediation: "SameSite=Lax または Strict を付与" + languages: + php: + patterns: + - type: regex + regex: 'setcookie\s*\([^)]*\)\s*;' + description: "setcookie に samesite が含まれない可能性 (要確認)" + - type: regex + regex: 'session\.cookie_samesite\s*=\s*[''"]?None' + description: "SameSite=None 指定 (Secure 必須)" + javascript: + patterns: + - type: regex + regex: 'sameSite\s*:\s*[''"]?none[''"]?' + description: "cookie sameSite:none 指定 (Secure 必須)" + + - id: IPA-SWS-1-SESS-007 + title: "Cookie の有効期限が長すぎる (4-(vi))" + severity: low + remediation_type: 保険的対策 + remediation: "Cookie 有効期限は短く (推奨 30 分〜数時間)、サーバ側タイムアウトも併用" + languages: + php: + patterns: + - type: regex + regex: 'time\(\)\s*\+\s*\d{7,}' + description: "Cookie 有効期限が約 4 ヶ月以上" + - type: regex + regex: 'session\.cookie_lifetime\s*=\s*\d{7,}' + description: "session.cookie_lifetime が長すぎる" + javascript: + patterns: + - type: regex + regex: 'maxAge\s*:\s*\d{10,}' + description: "maxAge が極端に大きい" + + - id: IPA-SWS-1-SESS-008 + title: "ログアウト処理でセッション破棄なし" + severity: medium + remediation_type: 根本的解決 + remediation: "ログアウト時に session_destroy() / invalidate() / Session.Abandon()" + languages: + php: + patterns: + - type: regex + regex: 'function\s+logout\s*\([^)]*\)\s*\{(?![^}]*session_destroy)' + description: "logout 関数に session_destroy がない (要確認)" + ruby: + patterns: + - type: regex + regex: 'def\s+logout\b(?!.*reset_session)' + description: "logout メソッドに reset_session がない (要確認)" + + - id: IPA-SWS-1-SESS-009 + title: "セッションIDが固定値 (4-(v))" + severity: high + remediation_type: 保険的対策 + remediation: "ログインごとに新規セッションID発行" + languages: + all: + patterns: + - type: regex + regex: '(session|sid|token)\s*=\s*[''"][a-zA-Z0-9]{8,}[''"]' + description: "セッションIDがハードコード文字列" diff --git a/skills/ipa-security-check/rules/sql_injection.yaml b/skills/ipa-security-check/rules/sql_injection.yaml new file mode 100644 index 0000000..e848a1f --- /dev/null +++ b/skills/ipa-security-check/rules/sql_injection.yaml @@ -0,0 +1,330 @@ +category: sql_injection +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.1 SQLインジェクション" + url: https://www.ipa.go.jp/security/vuln/websecurity/sql.html +cwe: CWE-89 + +rules: + - id: IPA-SWS-1-SQLI-001 + title: "文字列連結によるSQL組み立て (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "プレースホルダによる SQL 文の組み立て (1-(i)-a)" + languages: + php: + patterns: + - type: regex + regex: '\$_(GET|POST|REQUEST|COOKIE|SERVER)\[[^\]]+\][^;]*\.\s*[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|VALUES)\b' + description: "スーパーグローバルを SQL 様文字列に . で連結" + - type: regex + regex: '[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"]*[''"]\s*\.\s*\$\w+' + description: "SQL 文字列リテラルへの変数連結" + - type: regex + regex: '\b(mysql_query|mysqli_query|pg_query|->query|->exec)\s*\([^)]*\.\s*\$' + description: "クエリ実行関数に文字列連結を渡している" + - type: regex + regex: '[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"]*\$\w+[^''"]*[''"]' + description: "ダブルクォート内の変数展開で SQL 組み立て" + - type: regex + regex: 'sprintf\s*\(\s*[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"]*%[sd]' + description: "sprintf で SQL 文字列組み立て" + fix_example: + php: | + $stmt = $pdo->prepare("SELECT * FROM users WHERE id = :id"); + $stmt->execute([':id' => $_GET['id']]); + + - id: IPA-SWS-1-SQLI-002 + title: "文字列連結によるSQL組み立て (Java)" + severity: critical + remediation_type: 根本的解決 + remediation: "PreparedStatement と setXxx() による SQL 組み立て" + languages: + java: + patterns: + - type: regex + regex: '"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*"\s*\+\s*\w+' + description: "SQL 文字列に変数を + で連結" + - type: regex + regex: '\.execute(Query|Update)?\s*\(\s*"[^"]*"\s*\+' + description: "execute系メソッドに文字列連結結果を渡している" + - type: regex + regex: 'Statement\s+\w+\s*=\s*\w+\.createStatement\s*\(\s*\)' + description: "Statement.createStatement の使用 (PreparedStatement 推奨)" + - type: regex + regex: 'String\.format\s*\(\s*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b' + description: "String.format で SQL 組み立て" + - type: regex + regex: 'StringBuilder\s*\(\s*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b' + description: "StringBuilder で SQL を連結" + fix_example: + java: | + PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); + ps.setInt(1, userId); + + - id: IPA-SWS-1-SQLI-003 + title: "文字列連結によるSQL組み立て (Python)" + severity: critical + remediation_type: 根本的解決 + remediation: "cursor.execute(\"...\", (params,)) の引数渡しまたは ORM 利用" + languages: + python: + patterns: + - type: regex + regex: '\.execute\s*\(\s*[''"][^''"]*%[sd][''"][^)]*%' + description: "% 演算子による SQL 文字列構築" + - type: regex + regex: '\.execute\s*\(\s*f[''"]' + description: "f-string による SQL 構築" + - type: regex + regex: '\.execute\s*\(\s*[''"][^''"]*[''"]\s*\.\s*format\s*\(' + description: ".format() による SQL 構築" + - type: regex + regex: '\.execute\s*\(\s*[''"][^''"]*[''"]\s*\+' + description: "文字列連結による SQL 構築" + - type: regex + regex: '(cursor|cur|db|conn)\.execute\s*\(\s*[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"]*\{\w+\}' + description: "format フィールドを SQL に直接埋め込み" + fix_example: + python: | + cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,)) + + - id: IPA-SWS-1-SQLI-004 + title: "文字列補間による SQL 組み立て (Ruby)" + severity: critical + remediation_type: 根本的解決 + remediation: "ActiveRecord のクエリビルダや where('? = ?', val) を使用" + languages: + ruby: + patterns: + - type: regex + regex: '\.where\s*\(\s*[''"][^''"]*#\{' + description: "where 句に文字列補間" + - type: regex + regex: '\.(execute|exec_query|find_by_sql)\s*\(\s*[''"][^''"]*#\{' + description: "生 SQL 実行系に文字列補間" + - type: regex + regex: '[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"]*#\{[^}]+\}' + description: "SQL 文字列に #{} 展開" + fix_example: + ruby: | + User.where(id: params[:id]) + User.where('id = ?', params[:id]) + + - id: IPA-SWS-1-SQLI-005 + title: "文字列連結による SQL 組み立て (C#/.NET)" + severity: critical + remediation_type: 根本的解決 + remediation: "SqlCommand + Parameters.AddWithValue / Add で型指定" + languages: + csharp: + patterns: + - type: regex + regex: '"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*"\s*\+\s*\w+' + description: "文字列連結による SQL 構築" + - type: regex + regex: '\$"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*\{' + description: "C# 文字列補間 ($\"...\") による SQL 構築" + - type: regex + regex: 'String\.Format\s*\(\s*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b' + description: "String.Format での SQL 構築" + - type: regex + regex: 'new\s+SqlCommand\s*\(\s*"[^"]*"\s*\+' + description: "SqlCommand コンストラクタに動的 SQL" + - type: regex + regex: '"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^"]*"\s*&\s*\w+' + description: "VB.NET 文字列連結 (& 演算子) による SQL 構築" + fix_example: + csharp: | + var cmd = new SqlCommand("SELECT * FROM Users WHERE Id = @id", conn); + cmd.Parameters.Add("@id", SqlDbType.Int).Value = id; + + - id: IPA-SWS-1-SQLI-006 + title: "文字列連結による SQL 組み立て (Node.js/JavaScript/TypeScript)" + severity: critical + remediation_type: 根本的解決 + remediation: "パラメータ化クエリ (connection.query(sql, [params])) または ORM 使用" + languages: + javascript: + patterns: + - type: regex + regex: '\.(query|execute)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`' + description: "テンプレートリテラルで SQL 構築" + - type: regex + regex: '\.(query|execute)\s*\(\s*[''"][^''"]*[''"]\s*\+' + description: "クエリ関数に文字列連結を渡している" + - type: regex + regex: '[''"`][^''"`]*\b(SELECT|INSERT|UPDATE|DELETE)\b[^''"`]*[''"`]\s*\+\s*(req\.|request\.|params|user|input)' + description: "SQL リテラルにリクエスト由来の変数を + で連結" + typescript: + patterns: + - type: regex + regex: '\.(query|execute)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`' + description: "テンプレートリテラルで SQL 構築" + fix_example: + javascript: | + connection.query('SELECT * FROM users WHERE id = ?', [id], (err, rows) => { }); + + - id: IPA-SWS-1-SQLI-007 + title: "文字列連結による SQL 組み立て (Go)" + severity: critical + remediation_type: 根本的解決 + remediation: "db.Query(\"... ?\", arg) のプレースホルダ使用" + languages: + go: + patterns: + - type: regex + regex: 'db\.(Query|Exec|QueryRow)\s*\(\s*"[^"]*"\s*\+\s*\w+' + description: "db.Query に文字列連結" + - type: regex + regex: 'fmt\.Sprintf\s*\(\s*"[^"]*\b(SELECT|INSERT|UPDATE|DELETE)\b' + description: "fmt.Sprintf で SQL 構築" + fix_example: + go: | + rows, err := db.Query("SELECT * FROM users WHERE id = ?", id) + + - id: IPA-SWS-1-SQLI-008 + title: "危険なエスケープ関数のみへの依存 (PHP)" + severity: high + remediation_type: 根本的解決 + remediation: "addslashes / mysql_escape_string ではなくプレースホルダを使用" + languages: + php: + patterns: + - type: regex + regex: '\baddslashes\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE)' + description: "addslashes による SQL エスケープ (Shift_JIS 問題)" + - type: regex + regex: '\b(mysql_escape_string|mysql_real_escape_string)\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE)' + description: "古い mysql_(real_)escape_string への依存" + - type: regex + regex: '\bhtmlspecialchars\s*\([^)]+\)[^;]*[''"][^''"]*\b(SELECT|INSERT|UPDATE|DELETE)' + description: "htmlspecialchars を SQL エスケープとして誤用" + fix_example: + php: | + $stmt = $pdo->prepare('SELECT * FROM users WHERE name = ?'); + $stmt->execute([$_POST['name']]); + + - id: IPA-SWS-1-SQLI-009 + title: "識別子 (ORDER BY / テーブル名) への外部入力埋め込み" + severity: critical + remediation_type: 根本的解決 + remediation: "ホワイトリスト方式でカラム名・テーブル名を制限" + languages: + php: + patterns: + - type: regex + regex: 'ORDER\s+BY\s+[''"]?\s*\.\s*\$_(GET|POST|REQUEST)' + description: "ORDER BY 句に外部入力を連結" + - type: regex + regex: 'FROM\s+[''"]?\s*\.\s*\$_(GET|POST|REQUEST)' + description: "FROM 句に外部入力を連結" + - type: regex + regex: '\bORDER\s+BY\s+\$_(GET|POST|REQUEST)' + description: "ORDER BY 句にスーパーグローバル直挿入" + java: + patterns: + - type: regex + regex: 'ORDER\s+BY\s+"\s*\+\s*\w+' + description: "ORDER BY 句に変数を + 連結" + fix_example: + php: | + $allowed = ['id', 'name', 'created_at']; + $col = in_array($_GET['sort'], $allowed, true) ? $_GET['sort'] : 'id'; + $sql = "SELECT * FROM users ORDER BY $col"; + + - id: IPA-SWS-1-SQLI-010 + title: "hidden パラメータに SQL 文を指定 (論外パターン)" + severity: critical + remediation_type: 根本的解決 + remediation: "SQL 文をクライアントに渡さない (1-(ii))" + languages: + html: + patterns: + - type: regex + regex: ']+type=["'']?hidden["'']?[^>]+value=["''][^"'']*\b(SELECT|INSERT|UPDATE|DELETE)\b' + description: "hidden パラメータに SQL 文を指定" + + - id: IPA-SWS-1-SQLI-011 + title: "MySQL Connector/J で characterEncoding=sjis/ujis + 動的プレースホルダ (JVN#59748723)" + severity: high + remediation_type: 根本的解決 + remediation: "useServerPrepStmts=true を付与し、文字コードは utf8 を使用" + languages: + java: + patterns: + - type: regex + regex: 'jdbc:mysql:[^"'']*characterEncoding=(sjis|ujis)' + description: "古い MySQL JDBC で Shift_JIS / EUC-JP 指定" + - type: regex + regex: 'jdbc:mysql:[^"'']*(?!useServerPrepStmts=true)[^"'']*$' + description: "useServerPrepStmts 未指定の MySQL 接続文字列 (要確認)" + fix_example: + java: | + String url = "jdbc:mysql://HOST/DB?useUnicode=true&characterEncoding=utf8&useServerPrepStmts=true"; + + - id: IPA-SWS-1-SQLI-012 + title: "Perl DBD::mysql で SQL_INTEGER 指定の quote (数値検証されない)" + severity: high + remediation_type: 根本的解決 + remediation: "数値型キャスト ($v + 0) または bind_param + SQL_INTEGER の併用" + languages: + perl: + patterns: + - type: regex + regex: '->quote\s*\([^,]+,\s*SQL_INTEGER\s*\)' + description: "DBD::mysql の quote(..., SQL_INTEGER) は入力をそのまま返す" + + - id: IPA-SWS-1-SQLI-013 + title: "prepare 使用だが SQL 自体に文字列連結" + severity: high + remediation_type: 根本的解決 + remediation: "SQL リテラル内に変数を埋め込まず、必ずプレースホルダ位置に渡す" + languages: + php: + patterns: + - type: regex + regex: '->prepare\s*\(\s*[''"][^''"]*[''"]\s*\.\s*\$' + description: "PDO::prepare の引数に文字列連結" + java: + patterns: + - type: regex + regex: 'prepareStatement\s*\(\s*"[^"]*"\s*\+\s*\w+' + description: "prepareStatement に動的 SQL 文字列" + + - id: IPA-SWS-1-SQLI-014 + title: "エラーメッセージのブラウザ表示 (DB 種別/SQL 露出)" + severity: low + remediation_type: 保険的対策 + remediation: "本番では display_errors=Off / customErrors mode=On (1-(iii))" + languages: + php: + patterns: + - type: regex + regex: 'display_errors\s*=\s*(On|1|true)' + description: "display_errors が ON" + - type: regex + regex: '\b(echo|print|var_dump|print_r)\s*\(\s*\$\w*[Ee]rror' + description: "エラーオブジェクトを直接出力" + python: + patterns: + - type: regex + regex: '\bDEBUG\s*=\s*True' + description: "Django DEBUG=True" + + - id: IPA-SWS-1-SQLI-015 + title: "DB 接続が root/管理者権限ユーザ (1-(iv))" + severity: medium + remediation_type: 保険的対策 + remediation: "アプリ専用の最小権限 DB アカウントを使用" + languages: + php: + patterns: + - type: regex + regex: '(mysqli_connect|new\s+PDO|new\s+mysqli)\s*\([^)]*[''"]root[''"]' + description: "DB ユーザに root を指定" + yaml: + patterns: + - type: regex + regex: '(username|user):\s*[''"]?root[''"]?' + description: "設定ファイルの DB ユーザが root" diff --git a/skills/ipa-security-check/rules/web_health_check.yaml b/skills/ipa-security-check/rules/web_health_check.yaml new file mode 100644 index 0000000..d476eef --- /dev/null +++ b/skills/ipa-security-check/rules/web_health_check.yaml @@ -0,0 +1,280 @@ +category: web_health_check +ipa: + document: ウェブ健康診断仕様 + section: "13 診断項目 (A〜M)" + url: https://www.ipa.go.jp/security/vuln/websecurity/ug65p900000196e2-att/000017319.pdf + +rules: + - id: IPA-WHC-A-001 + title: "(A) SQLインジェクション診断パターン (動的検査の静的ヒント)" + severity: critical + remediation_type: 根本的解決 + remediation: "文字列連結 SQL を撤廃しプレースホルダを使用" + languages: + all: + patterns: + - type: regex + regex: '(SELECT|INSERT|UPDATE|DELETE|WHERE|FROM)[^;]*[''"]\s*[+&.]\s*\w+' + description: "文字列連結による SQL 組み立て (汎用)" + - type: regex + regex: '(SELECT|INSERT|UPDATE|DELETE)[^`''"]*\$\{[^}]+\}' + description: "テンプレートリテラル/f-string で SQL 組み立て" + - type: regex + regex: '\.execute\(\s*[''"][^''"]*[''"]\s*%\s*' + description: "% 演算子で SQL 組み立て (Python)" + - type: regex + regex: '\.execute\(\s*[''"][^''"]*[''"]\s*\.format\(' + description: ".format() で SQL 組み立て (Python)" + + - id: IPA-WHC-B-001 + title: "(B) XSS 診断対象 (入力内容確認・エラー画面)" + severity: high + remediation_type: 根本的解決 + remediation: "全出力でエスケープ" + languages: + all: + patterns: + - type: regex + regex: '<%=\s*request\.getParameter' + description: "JSP 生出力" + - type: regex + regex: 'echo\s+\$_(GET|POST|REQUEST|COOKIE)' + description: "PHP 生出力" + - type: regex + regex: 'dangerouslySetInnerHTML' + description: "React dangerouslySetInnerHTML" + - type: regex + regex: '\bv-html\b' + description: "Vue v-html" + - type: regex + regex: '\[innerHTML\]\s*=' + description: "Angular [innerHTML]" + - type: regex + regex: 'document\.write\s*\(' + description: "document.write 使用" + + - id: IPA-WHC-C-001 + title: "(C) CSRF 診断 (重要処理にトークン検証)" + severity: high + remediation_type: 根本的解決 + remediation: "hidden トークン + サーバ側検証" + languages: + python: + patterns: + - type: regex + regex: '@csrf_exempt\b' + description: "Django @csrf_exempt" + ruby: + patterns: + - type: regex + regex: 'skip_before_action\s*:verify_authenticity_token' + description: "Rails CSRF スキップ" + java: + patterns: + - type: regex + regex: '\.csrf\s*\(\s*\)\s*\.disable\s*\(' + description: "Spring CSRF disable" + + - id: IPA-WHC-D-001 + title: "(D) OSコマンド・インジェクション診断" + severity: critical + remediation_type: 根本的解決 + remediation: "シェル経由のコマンド呼び出しを避ける" + languages: + all: + patterns: + - type: regex + regex: '\b(system|exec|popen|passthru|shell_exec|proc_open)\s*\(' + description: "シェル起動関数の使用" + - type: regex + regex: 'subprocess\.(call|run|Popen|check_output)[^)]*shell\s*=\s*True' + description: "subprocess shell=True" + - type: regex + regex: 'child_process\.(exec|execSync)\s*\(' + description: "Node.js child_process.exec" + - type: regex + regex: 'Runtime\.getRuntime\(\)\.exec\s*\(\s*"[^"]*"\s*\+' + description: "Java Runtime.exec に文字列連結" + + - id: IPA-WHC-E-001 + title: "(E) ディレクトリ・リスティング (Apache/Nginx/IIS 設定)" + severity: medium + remediation_type: 根本的解決 + remediation: "Options -Indexes / autoindex off / directoryBrowse=false" + languages: + apache: + patterns: + - type: regex + regex: '^[^#]*Options\s+[^#\n]*\bIndexes\b' + description: "Apache Options Indexes 有効" + nginx: + patterns: + - type: regex + regex: '^[^#]*autoindex\s+on' + description: "Nginx autoindex on" + javascript: + patterns: + - type: regex + regex: 'require\s*\(\s*[''"]serve-index[''"]' + description: "Express serve-index ミドルウェア" + xml: + patterns: + - type: regex + regex: 'listings\s*true' + description: "Tomcat DefaultServlet listings=true" + + - id: IPA-WHC-F-001 + title: "(F) メールヘッダ・インジェクション診断" + severity: critical + remediation_type: 根本的解決 + remediation: "宛先固定 + 改行除去" + languages: + php: + patterns: + - type: regex + regex: 'mail\s*\([^)]*,\s*[^)]*,\s*[^)]*,\s*[''"][^''"]*\$_(GET|POST|REQUEST)' + description: "mail() の追加ヘッダにスーパーグローバル" + - type: regex + regex: '(Subject|From|To|Cc|Bcc)\s*[:=]\s*\$_(GET|POST|REQUEST)' + description: "ヘッダにスーパーグローバル直代入" + + - id: IPA-WHC-G-001 + title: "(G) ディレクトリ・トラバーサル診断" + severity: critical + remediation_type: 根本的解決 + remediation: "basename + realpath + ベース確認" + languages: + all: + patterns: + - type: regex + regex: '(file_get_contents|fopen|readfile|include|require|require_once|include_once)\s*\([^)]*\$_(GET|POST|REQUEST|COOKIE)' + description: "ファイル系 API にスーパーグローバル" + - type: regex + regex: '\bopen\s*\(\s*request\.(GET|POST|args|form|params)' + description: "open に Python request 直渡し" + - type: regex + regex: 'fs\.(readFile|createReadStream|sendFile|readFileSync)\s*\([^)]*req\.(query|body|params)' + description: "fs API に req 直渡し" + + - id: IPA-WHC-H-001 + title: "(H) 意図しないリダイレクト診断" + severity: high + remediation_type: 根本的解決 + remediation: "ホワイトリスト URL のみ許可" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"]Location:\s*[''"]\s*\.\s*\$_(GET|POST|REQUEST)' + description: "Location ヘッダに外部入力連結" + python: + patterns: + - type: regex + regex: 'redirect\s*\(\s*request\.(args|GET|POST|values)' + description: "redirect に request 直渡し" + javascript: + patterns: + - type: regex + regex: 'res\.redirect\s*\(\s*req\.(query|body|params)' + description: "res.redirect に req 直渡し" + - type: regex + regex: 'location\.(href|assign|replace)\s*=\s*\w+\.value' + description: "location.href にフォーム値直代入" + + - id: IPA-WHC-I-001 + title: "(I) HTTPヘッダ・インジェクション診断" + severity: high + remediation_type: 根本的解決 + remediation: "CRLF 検査 + ヘッダ出力 API 使用" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"][^''"]*[''"]\s*\.\s*\$_(GET|POST|REQUEST)' + description: "header() に外部入力連結" + - type: regex + regex: 'setcookie\s*\(\s*[^,)]*,\s*\$_(GET|POST|REQUEST)' + description: "setcookie 値に外部入力直渡し" + + - id: IPA-WHC-J-001 + title: "(J) 認証の診断 (パスワードポリシー / アカウントロック)" + severity: medium + remediation_type: 根本的解決 + remediation: "8文字以上、英数字混在、アカウントロック、共通エラーメッセージ" + languages: + html: + patterns: + - type: regex + regex: ']*name=["'']?password["''][^>]*type=["'']text["'']' + description: "パスワード入力欄が type=text (伏字でない)" + - type: regex + regex: '(minlength|min_length|min)\s*[:=]\s*[1-7]\b' + description: "パスワード最小長が 8 未満" + all: + patterns: + - type: regex + regex: '[''"](ユーザ?ID|user(name|_id))[^''"]*(存在しない|not\s+found|invalid)' + description: "ユーザID限定のエラーメッセージ (粒度過剰)" + - type: regex + regex: '\bmd5\s*\(\s*\$?password\b' + description: "パスワードを md5 でハッシュ (弱い)" + - type: regex + regex: '\bsha1\s*\(\s*\$?password\b' + description: "パスワードを sha1 でハッシュ (弱い)" + + - id: IPA-WHC-K-001 + title: "(K) セッション管理の不備 (健康診断)" + severity: high + remediation_type: 根本的解決 + remediation: "フレームワーク標準セッション機構 + Secure/HttpOnly + 再生成" + languages: + php: + patterns: + - type: regex + regex: 'session\.use_trans_sid\s*=\s*1' + description: "PHP session.use_trans_sid=1 (URL Rewriting)" + - type: regex + regex: 'session\.cookie_secure\s*=\s*(0|false|off)' + description: "PHP cookie_secure 無効" + - type: regex + regex: 'session\.cookie_httponly\s*=\s*(0|false|off)' + description: "PHP cookie_httponly 無効" + + - id: IPA-WHC-L-001 + title: "(L) 認可制御の不備、欠落 (健康診断)" + severity: critical + remediation_type: 根本的解決 + remediation: "所有者検証 (WHERE user_id = current_user) + ロール検証" + languages: + ruby: + patterns: + - type: regex + regex: '\w+\.find\s*\(\s*params\[:id\]\s*\)' + description: "Model.find(params[:id]) で current_user スコープなし" + php: + patterns: + - type: regex + regex: '\$_(COOKIE|POST|GET)\[[''"](role|admin|is_admin|permission)[''"]\]' + description: "権限情報をクライアント送信値から取得" + + - id: IPA-WHC-M-001 + title: "(M) クローラ耐性 (N+1 / 全件取得)" + severity: low + remediation_type: 保険的対策 + remediation: "ページネーション、N+1 解消、レートリミット、キャッシュ" + languages: + python: + patterns: + - type: regex + regex: '\.objects\.all\s*\(\s*\)(?!.*\[)' + description: "Django .objects.all() (LIMIT/スライスなし)" + ruby: + patterns: + - type: regex + regex: '\b\w+\.all\b(?!.*\.limit)' + description: "Rails .all (limit なし)" + all: + patterns: + - type: regex + regex: 'SELECT\s+\*\s+FROM\s+\w+(?![\s\S]{0,100}LIMIT)' + description: "SELECT * FROM ... に LIMIT なし" diff --git a/skills/ipa-security-check/rules/xss.yaml b/skills/ipa-security-check/rules/xss.yaml new file mode 100644 index 0000000..f1bcc22 --- /dev/null +++ b/skills/ipa-security-check/rules/xss.yaml @@ -0,0 +1,279 @@ +category: xss +ipa: + document: 安全なウェブサイトの作り方 改訂第7版 + section: "1.5 クロスサイト・スクリプティング" + url: https://www.ipa.go.jp/security/vuln/websecurity/cross-site-scripting.html +cwe: CWE-79 + +rules: + - id: IPA-SWS-1-XSS-001 + title: "エスケープなしの直接出力 (PHP)" + severity: critical + remediation_type: 根本的解決 + remediation: "htmlspecialchars($v, ENT_QUOTES | ENT_HTML5, 'UTF-8') を必ず通す (5-(i))" + languages: + php: + patterns: + - type: regex + regex: '\becho\s+\$_(GET|POST|REQUEST|COOKIE|SERVER)\b' + description: "echo でスーパーグローバル直出力" + - type: regex + regex: '\bprint\s+\$_(GET|POST|REQUEST|COOKIE|SERVER)\b' + description: "print でスーパーグローバル直出力" + - type: regex + regex: '<\?=\s*\$_(GET|POST|REQUEST|COOKIE|SERVER)' + description: " または fn:escapeXml を使用" + languages: + java: + patterns: + - type: regex + regex: '<%=\s*(request\.|param\.|session\.).*?%>' + description: "JSP scriptlet で未エスケープ出力" + - type: regex + regex: 'out\.println\s*\(\s*request\.getParameter\(' + description: "out.println に request.getParameter 直渡し" + - type: regex + regex: 'th:utext\s*=' + description: "Thymeleaf th:utext (Unescaped) の使用" + + - id: IPA-SWS-1-XSS-003 + title: "html_safe / raw による未エスケープ (Ruby / Rails)" + severity: critical + remediation_type: 根本的解決 + remediation: "自動エスケープに任せる。raw / html_safe は信頼できる文字列のみ" + languages: + ruby: + patterns: + - type: regex + regex: '\.html_safe\b' + description: ".html_safe 呼び出し" + - type: regex + regex: '\braw\s*\(' + description: "raw() 呼び出し" + - type: regex + regex: '\braw\s+\w+' + description: "raw <変数> 形式" + - type: regex + regex: '<%==\s' + description: "<%== でのエスケープ無効出力" + + - id: IPA-SWS-1-XSS-004 + title: "React dangerouslySetInnerHTML" + severity: high + remediation_type: 根本的解決 + remediation: "原則禁止。必要な場合は DOMPurify でサニタイズ" + languages: + javascript: + patterns: + - type: regex + regex: 'dangerouslySetInnerHTML\s*=' + description: "React dangerouslySetInnerHTML 使用" + typescript: + patterns: + - type: regex + regex: 'dangerouslySetInnerHTML\s*=' + description: "React dangerouslySetInnerHTML 使用" + + - id: IPA-SWS-1-XSS-005 + title: "Vue v-html / Angular [innerHTML]" + severity: high + remediation_type: 根本的解決 + remediation: "原則禁止。Angular は DomSanitizer で明示的検査" + languages: + javascript: + patterns: + - type: regex + regex: '\bv-html\s*=' + description: "Vue v-html ディレクティブ使用" + - type: regex + regex: '\[innerHTML\]\s*=' + description: "Angular [innerHTML] 使用" + - type: regex + regex: 'bypassSecurityTrust(Html|Script|Style|Url|ResourceUrl)\b' + description: "Angular bypassSecurityTrust* 使用" + + - id: IPA-SWS-1-XSS-006 + title: "DOM Based XSS の危険 DOM API" + severity: critical + remediation_type: 根本的解決 + remediation: "textContent や DOM API を使用し、innerHTML 直書きを避ける" + languages: + javascript: + patterns: + - type: regex + regex: '\.innerHTML\s*=' + description: ".innerHTML への代入" + - type: regex + regex: '\.outerHTML\s*=' + description: ".outerHTML への代入" + - type: regex + regex: '\binsertAdjacentHTML\s*\(' + description: "insertAdjacentHTML 使用" + - type: regex + regex: '\bdocument\.write(ln)?\s*\(' + description: "document.write(ln) 使用" + - type: regex + regex: '\beval\s*\(' + description: "eval() 使用" + - type: regex + regex: '\bnew\s+Function\s*\(' + description: "new Function() 使用" + - type: regex + regex: '\bsetTimeout\s*\(\s*[''"`]' + description: "setTimeout 第1引数に文字列" + - type: regex + regex: '\bsetInterval\s*\(\s*[''"`]' + description: "setInterval 第1引数に文字列" + - type: regex + regex: 'location\.(hash|search|href)[^=]*\.(innerHTML|outerHTML|src)' + description: "location.* の値を DOM へ書き込み" + - type: regex + regex: '\bwindow\.name\b' + description: "window.name は XSS の入力経路" + - type: regex + regex: '\$\([^)]+\)\.html\s*\(' + description: "jQuery .html() への動的代入" + typescript: + patterns: + - type: regex + regex: '\.innerHTML\s*=' + description: ".innerHTML への代入" + + - id: IPA-SWS-1-XSS-007 + title: "URL 属性 (href/src) への外部入力 (5-(ii))" + severity: high + remediation_type: 根本的解決 + remediation: "http(s):// で始まる URL のみ許可 (スキームをホワイトリスト)" + languages: + javascript: + patterns: + - type: regex + regex: '\.(href|src|action|formaction)\s*=\s*[^;]+(location|input|param|query|user)' + description: "href/src に未検証 URL 代入" + - type: regex + regex: 'href\s*=\s*["'']?javascript:' + description: "href に javascript: スキーム" + - type: regex + regex: 'src\s*=\s*["'']?javascript:' + description: "src に javascript: スキーム" + html: + patterns: + - type: regex + regex: 'href\s*=\s*["'']?\$\{[^}]+\}' + description: "テンプレートで href にプレースホルダ (スキーム検査要)" + + - id: IPA-SWS-1-XSS-008 + title: "Content-Type に charset 未指定 (5-(viii))" + severity: medium + remediation_type: 根本的解決 + remediation: "Content-Type に charset=UTF-8 を明示" + languages: + php: + patterns: + - type: regex + regex: 'header\s*\(\s*[''"]Content-Type:\s*text/html[''"]?\s*\)' + description: "Content-Type: text/html に charset がない" + all: + patterns: + - type: regex + regex: 'Content-Type:\s*text/html(?![^\n]*charset)' + description: "Content-Type: text/html に charset が含まれない" + + - id: IPA-SWS-1-XSS-009 + title: "