Claude Media
Claude CodeでSECURITY.mdと脆弱性報告の初動を整える

Claude CodeでSECURITY.mdと脆弱性報告の初動を整える

脆弱性報告を受けたメンテナ向けに、SECURITY.mdと非公開報告の準備、公開側に漏らさない権限設計、再現と影響バージョンの絞り込み、advisory下書きまでを順に扱います。

OSSのメンテナが脆弱性の報告を受けたとき、最初の数時間で決めることは3つあります。報告を再現できるか、どのバージョンまで影響するか、そして途中経過を公開側に漏らさないか。ここまでの調査と下書きはClaude Codeに任せられますが、任せ方を誤ると、修正前の脆弱性がpublicなissueやPRに出ます。

この記事は、メンテナ側の受け付けを扱います。事前準備としてSECURITY.mdと非公開の報告窓口を整え、報告が届いたら再現、影響バージョンの絞り込み、非公開のsecurity advisoryの下書きまで進めます。自社のリポジトリが他人のCVEに該当するかを調べる利用者側の話は、CVEの影響調査の記事が扱っています。

受け付けの入口を先に整える

報告者が迷わず非公開で連絡できる状態にするのが先です。GitHubのpublicリポジトリでは、入口が2つあります。

入口役割設定する場所
SECURITY.md役割対応バージョンと報告方法を伝える設定する場所リポジトリのSecurity policy
private vulnerability reporting役割報告者がフォームから非公開で報告する設定する場所SettingsのAdvanced Security

SECURITY.mdは、「Security and quality」タブ(旧Securityタブ)の左サイドバーにある「Security policy」から「Start setup」で作れます。GitHubのドキュメントが書くべき内容として挙げているのは、サポート対象のバージョンと、脆弱性の報告方法の2点です。

private vulnerability reportingを有効にすると、報告者にはリポジトリのAdvisoriesページに「Report a vulnerability」ボタンが出ます。有効化できるのはリポジトリのオーナー、organizationのオーナー、セキュリティマネージャー、admin権限のユーザーです。

SECURITY.mdは、たとえば次の形にまとめると、報告者が公開issueに書く事故を減らせます。これは書式の一例で、GitHubが定める固定フォーマットではありません。

# Security Policy
 
## Supported Versions
 
| Version | Supported |
|---|---|
| 2.x | yes |
| 1.x | security fixes only |
| < 1.0 | no |
 
## Reporting a Vulnerability
 
公開issueやPull Requestには書かないでください。
このリポジトリの Security タブから
「Report a vulnerability」で非公開に報告してください。
 
再現手順、影響を受けるバージョン、想定される影響を
書いていただけると調査が早く進みます。

報告フォームで必要な情報を先に集める

private vulnerability reportingの既定のフォームは、概要、詳細、再現のための概念実証(proof of concept)、影響の記述を必須にしています。足りない情報を後から聞き返す手間を減らすためで、項目を足したいときはリポジトリの.githubディレクトリにVULNERABILITY_REPORT.yml(または.yaml)を置きます。構文はissue formsと同じで、input、textarea、dropdown、checkboxes、markdownの要素が使えます。入力とtextareaにはmin_lengthで最小文字数も指定できます。

owner側の.githubリポジトリに置けば、そのアカウント配下の既定のフォームになります。個別リポジトリのフォームが優先されます。

# .github/VULNERABILITY_REPORT.yml(書式の一例)
body:
  - type: input
    id: affected-version
    attributes:
      label: 確認したバージョン
    validations:
      required: true
  - type: textarea
    id: reproduce
    attributes:
      label: 再現手順
      description: コマンドと入力を、そのまま実行できる形で
    validations:
      required: true

公開側に漏らさない権限設計

報告の内容は、公開される前のsecurity advisoryの中にあります。Claude Codeに触らせるときは、次の経路を塞ぎます。

  • 報告の本文や再現コードが、publicなissueやPRのコメントに出ること
  • 修正のコミットが、公開ブランチに先に出ること
  • 調査メモが、リポジトリの追跡対象に入ること

報告の本文はセッションに貼る側で絞る

最も確実なのは、Claude Codeにadvisoryそのものを取りに行かせないことです。画面で読んだ報告のうち、再現に必要な部分だけをメンテナが貼ります。GitHubのREST APIでもadvisoryの一覧や作成はできますが、非公開のadvisoryを読むにはclassic tokenならrepoかrepository_advisories:readのスコープ、fine-grainedトークンなら「Repository security advisories」のread権限が要ります。セッションにこのトークンを渡せば、読めるのは報告だけではなく同じ権限で届く範囲すべてになります。

報告の全文をセッションに貼れば、それはモデルへの入力です。報告者の氏名や連絡先、他の未公開の情報まで貼る必要はありません。再現に要る部分だけに絞ります。

公開側へ出る操作を設定で止める

Claude Codeの権限ルールは、deny、ask、allowの順に評価され、最初に当たったものが結果になります。denyに入れたルールを、より狭いallowで例外にすることはできません。この性質を使い、公開側に出る操作はdenyかaskに置きます。

{
  "permissions": {
    "deny": [
      "Bash(git push upstream *)",
      "Bash(gh issue *)",
      "Bash(gh pr *)"
    ],
    "ask": [
      "Bash(git push *)"
    ]
  }
}

Bash(git push *)の末尾の*は、引数なしのgit pushにも一致します。ここでは、公開リポジトリのリモートをupstreamという名前にそろえ、そこへのpushをdenyで固定しています。temporary private forkをcloneすると、そのリモートはoriginになります。fork側へのpushはaskに回り、プロンプトの宛先を目で確かめてから承認する流れです。公開リポジトリをoriginにしたままの作業コピーでは、このdenyが効かないので、リモート名は必ず確かめてください。

ただし、Bashのdenyルールはセキュリティの境界ではありません。Claude Codeはコマンドの文字列に対して照合するので、たとえばcurlをdenyに入れても、フルパス指定やsh -c経由の呼び出しは止められないと権限ドキュメントが明記しています。git push upstreamも同じ型の抜け道を持ちます。そのため、設定だけで守れた気にならず、次の3点を重ねます。

  1. 作業ディレクトリは、temporary private forkをcloneした場所に限る(公開リポジトリの作業コピーと分ける。公開側のリモートを足すならupstreamの名前で)
  2. ghの認証は、このセッションでは使わない
  3. 調査メモは、リポジトリの外のディレクトリに置く

サブエージェントで道具を絞る

再現の検証だけを任せたいなら、サブエージェントのtoolsで許可リストを絞れます。編集系ツール(EditとWrite)を外した調査役の例です。Bashは残るので、ネットワークや書き込みは前節のpermissions.denyと作業ディレクトリの分離で止めます。

---
name: vuln-reproducer
description: 報告された脆弱性の再現手順を読み、ローカルで再現できるかだけを確かめる
tools: Read, Grep, Glob, Bash
---
 
報告の再現手順をローカルで実行し、再現したかどうかと根拠を報告する。
ソースの編集、git push、外部へのリクエストはしない。

注意点として、disallowedToolsにBash(git push *)のような指定付きの項目を入れても、サブエージェントからはBashツールごと外れます。コマンド単位で止めたいときは、上のようにsettingsのpermissions.denyへ書きます。このルールはメインの会話にもサブエージェントにも効きます。

落とし穴

調査メモをリポジトリ内に置くと、公開コミットに混ざる

報告の再現スクリプトや考察をnotes/のような追跡対象のディレクトリに書かせると、次の通常のコミットで公開されます。メモの置き場はリポジトリの外にして、作業コピーのgit statusを毎回確かめます。シークレットをコミットした後の対応で扱うとおり、一度公開した値は履歴から消してもすでに見られた可能性が残ります。

報告を再現し、影響バージョンを絞る

入口と権限が整ったら、報告が届いてからの作業に入ります。順序は次のとおりです。

手順

報告を受けてからの初動

  1. 1

    報告を読み、再現手順を実行形にする

    報告の文章を、コマンドと入力の並びに直します。曖昧な箇所は推測で埋めず、報告者への質問として切り出します。

  2. 2

    最新版で再現するかを確かめる

    まず最新のリリースで再現するかを見ます。再現しなければ、すでに修正済みか、環境依存の可能性があります。

  3. 3

    導入されたコミットを特定する

    再現スクリプトを判定に使い、悪いコミットを二分探索で絞ります。

  4. 4

    影響するバージョンの範囲を決める

    導入コミットを含む最初のタグと、修正を含む最初のタグから範囲を決めます。

再現スクリプトを判定に使って二分探索する

影響バージョンの絞り込みは、Gitの二分探索に任せるのが速くて確実です。Claude Codeには、再現手順を終了コードに変換するスクリプトを書かせます。脆弱性が再現したら非0、しなければ0を返す形にします。

#!/usr/bin/env bash
# repro.sh: 再現したら 1、しなければ 0 を返す(報告ごとに書く)
./build.sh >/dev/null 2>&1 || exit 125   # ビルド不能は 125 でスキップ
./run-poc.sh | grep -q "VULNERABLE" && exit 1 || exit 0
git bisect start <再現するバージョン> <再現しないバージョン>
git bisect run ./repro.sh
git bisect reset

git bisect runは、終了コード0を「良い」、1〜127(125を除く)を「悪い」、125を「判定不能」として扱います。ビルドが通らないコミットは125で飛ばすのはそのためです。判定スクリプト自体が正しくないと、結果もそのまま誤ります。Claude Codeが書いたスクリプトは、再現すると分かっているバージョンと、再現しないと分かっているバージョンの両方で一度ずつ手で回してから、二分探索に使います。

タグを範囲に変える

二分探索で出た導入コミットが、どのリリースに入ったかを調べます。

git tag --contains <導入コミット> --sort=version:refname | head -3
git tag --contains <修正コミット> --sort=version:refname | head -3

前者の最初のタグが影響範囲の下限、後者の最初のタグが修正済みバージョンです。保守ブランチ(1.xなど)にバックポートした場合は、ブランチごとに同じ調査が要ります。ここでの数え漏れが、advisoryの「影響を受けないはずのバージョン」を生むので、バージョンの一覧は人の目でも突き合わせます。

advisoryでのバージョンの書き方

GitHubのAffected versionsの欄は書式が決まっています。下限と上限を両方書くときは、下限が先、カンマと半角スペース1つを挟んで上限です。演算子とバージョンの間は半角スペース1つ入れます。

書き方意味
>= 1.2.0, < 1.4.3意味1.2.0以上、1.4.3未満
< 2.0.1意味2.0.1未満すべて
= 1.3.0意味1.3.0のみ

演算子は>=、>、=、<=、<が使えます。>は使えますが、OSV形式が対応していないため推奨されていません。また、修正済みバージョン(Fixed)に、脆弱な範囲の最大値より小さいバージョンは指定できません。プレリリースは、数字の後ろに文字が付くバージョン(2.0.0-rcなど)が正式版より前のバージョンとして扱われます。

この書式で書いておくと、公開時にGitHub Advisory Databaseの「GitHub-reviewed」advisoryとして追加されやすく、Dependabotが影響を受けるリポジトリを正確に判定できるとGitHubは説明しています。

非公開のadvisoryに下書きを載せる

調査結果が揃ったら、security advisoryの下書き(draft)を作ります。「Security and quality」タブの「Advisories」から「New draft security advisory」を開き、必須項目(アスタリスク付き)を埋めます。Claude Codeには、調査結果をこのフォームの項目に当てはめた文面を作らせます。

フォームの項目下書きに書く内容誰が決めるか
Title下書きに書く内容脆弱性の内容が分かる短い題誰が決めるかメンテナ
CVE identifier下書きに書く内容既存のCVEを使うか、GitHubに後で依頼するか誰が決めるかメンテナ
Description下書きに書く内容影響、利用できるパッチや回避策、参考情報誰が決めるかClaudeが下書き、メンテナが確認
Affected products下書きに書く内容エコシステム、パッケージ名、影響するバージョン、修正バージョン、脆弱な関数誰が決めるか二分探索の結果を元に
Severity下書きに書く内容深刻度。CVSSで計算することもできる誰が決めるかメンテナ
Weaknesses下書きに書く内容CWE(脆弱性の種類の分類)誰が決めるかClaudeが候補、メンテナが選ぶ
Credits下書きに書く内容発見者や報告者への謝辞誰が決めるかメンテナ

DescriptionとAffected productsは、調査結果の整理なので下書きを任せやすい部分です。一方、深刻度とCWEは、Claudeが候補を出すのは構いませんが、確定はメンテナです。深刻度の値は、利用者の対応の優先度に直結します。

謝辞の種類を使い分ける

Creditsには、発見や修正に関わった人を載せられます。種類は次の9つです。

種類理由
Finder理由脆弱性を見つけた
Reporter理由ベンダーやCNAに脆弱性を通知した
Analyst理由正確さや深刻度を検証した
Coordinator理由調整プロセスを進めた
Remediation developer理由修正のコードや計画を用意した
Remediation reviewer理由修正案の有効性と網羅性をレビューした
Remediation verifier理由脆弱性や修正をテストして確認した
Tool理由発見や特定に使ったツール
Sponsor理由特定や修正の活動を支援した

謝辞を付けた相手にはメールかWebで通知が届き、承諾すると、advisoryの公開後にユーザー名が表示されます。辞退もできます。

修正はtemporary private forkで進める

修正のコードも、公開リポジトリのブランチでは作りません。draftのadvisoryから「Start a temporary private fork」を押すと、リポジトリ名-ghsa-xxxx-xxxx-xxxxという名前の非公開フォークができます。cloneの手順はadvisoryのページに出ます。

このフォークは、漏れを避けるために次の制約があります。

  • CIを含むインテグレーションからはアクセスできない
  • プルリクエストのステータスチェックは実行されない
  • 個別のPRはマージできず、advisoryの画面で開いているPRをまとめてマージする
  • マージ時、ブランチ保護ルールは適用されない

CIが走らないので、修正のテストはローカルで回します。ここはClaude Codeの得意な領域で、回帰テストの追加と実行を任せられます。修正PRの見方は、セキュリティ修正のPRレビューにまとめています。ブランチ保護が効かない以上、マージ前の最終確認はメンテナが行います。

公開前に確かめること

advisoryを公開する前に、次の点を見ます。

公開ボタンの前に

  • Affected productsに修正済みバージョンを入れたか
  • 修正を含むリリースがすでに配布されているか
  • Creditsの相手に承諾を求める通知が届いているか
  • CVEが必要なら、依頼の操作を済ませたか

修正版を入れずに公開すると、修正済みバージョンが書かれていないままDependabotが利用者に警告を出し、更新先の安全なバージョンを示せなくなります。修正がすぐに出せるなら公開を待ち、開発中なら、その旨をadvisoryに書いて公開後に編集します。修正の予定がない場合は、そのことと、利用者が取れる緩和策を明記しておきます。

CVEは、GitHubに依頼できます。GitHubによるCVE依頼の審査には通常72時間ほどかかり、依頼しただけではadvisoryは公開されません。既に他のCNAでCVEを取っている場合は、その番号をフォームに入れます。CVE依頼は、draftの画面の「Request CVE」ボタンで行います。

公開(Publish advisory)すると、temporary private forkは削除されます。公開後は誰でもadvisoryの現在の内容を読めますが、会話の履歴を読めるのはコラボレーターだけです。公開した内容の誤りは編集で直せますが、誤って公開したadvisoryの削除はGitHub Supportへの連絡が必要です。

GitHubはAdvisory Databaseへの追加を審査し、Dependabot alertを送ることがあります。Advisory Databaseへの追加の審査と通知には、最大72時間かかることがあります。CVEの審査とは別の処理です。

よくある質問

GitHubを使っていないリポジトリでも同じですか

ここで扱ったadvisoryとprivate vulnerability reportingは、GitHub.comのpublicリポジトリ向けの機能です。権限設計の考え方(公開側に出る操作を止める、メモを追跡対象の外に置く)は、他のホスティングでも通じます。

まとめ

脆弱性報告の初動で任せやすいのは、再現スクリプトの作成、二分探索による範囲の絞り込み、advisory下書きの整形です。判断を残すのは、深刻度、CWE、公開のタイミングです。

仕組みとしては、公開側に出るpushやissue操作をdenyかaskに置き、調査メモをリポジトリの外に置きます。設定は穴を完全に塞げないので、承認プロンプトの宛先を見る習慣が最後の防御になります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. docs.github.com/en/code-security/security-advisories/working-with-reposito…
  2. docs.github.com/en/code-security/security-advisories/working-with-reposito…
  3. docs.github.com/en/code-security/security-advisories/working-with-reposito…
  4. docs.github.com/en/code-security/security-advisories/working-with-reposito…
  5. docs.github.com/en/rest/security-advisories/repository-advisories
  6. code.claude.com/docs/en/permissions
  7. code.claude.com/docs/en/sub-agents
この記事を共有:XはてブLinkedIn