Claude CodeでSAML SPの署名検証を点検する — Audienceと署名ラッピング
SAML SPが検証すべき署名・Audience・Recipient・InResponseTo・期限を表にし、Claude Codeに点検させる手順と、改ざんレスポンスを拒否するテストの書き方を示します。
SAML SP(Service Provider)は、IdP(Identity Provider)が発行したAssertionを受け取り、ログインを成立させる側です。自社SaaSをSAML対応にするとき、事故になりやすいのはIdP側の設定ではなくSP側の検証コードです。署名を検証しているつもりで、検証した要素と実際にユーザー情報を読み出す要素が別物、という実装が典型です。
この記事は、そのAssertion検証コードをClaude Codeに点検させる手順を扱います。点検の物差しはOASISのSAML 2.0プロファイル仕様が定める検証項目です。Claude Codeには「読むだけ」の権限でコードを見せ、改ざんしたレスポンスで必ず失敗するテストを書かせて、実行結果で確かめます。なお、ClaudeのSSOをIdPと接続する手順はClaude SSOの設定手順にあり、ここで扱うのはその反対側、自社アプリを受け側にする実装の話です。
SPが必ず検証する項目はプロファイル仕様に書いてある
SAML 2.0のWeb Browser SSOプロファイルは、SPが<Response>を処理するときの規則を列挙しています。点検の物差しはこの一覧です。Claude Codeに「セキュリティ的におかしいところを探して」と頼むより、検証項目を先に渡して「この実装はどの項目をどこで確認しているか」を答えさせるほうが、抜けが見つかりやすくなります。
仕様が挙げる検証は次のとおりです。
| 検証対象 | 仕様の要求 |
|---|---|
| 署名 | 仕様の要求AssertionまたはResponseに付いた署名を検証する。HTTP POSTバインディングではAssertionに署名が必須 |
| Recipient | 仕様の要求bearerのSubjectConfirmationDataのRecipientが、受信したACS URLと一致する |
| NotOnOrAfter | 仕様の要求SubjectConfirmationDataのNotOnOrAfterを過ぎていない(時計のずれは許容範囲で考慮) |
| InResponseTo | 仕様の要求SubjectConfirmationDataのInResponseToが、元のAuthnRequestのIDと一致する |
| Audience | 仕様の要求bearerを含むAssertionのAudienceRestrictionに、SP自身の識別子がAudienceとして入っている |
| 再利用 | 仕様の要求POSTバインディングでは、使用済みのID値を保持し、Assertionの有効期間のあいだ再利用を拒否する |
この表の後半は、署名が正しくても通してしまう事故の原因になります。署名はIdPが発行した事実しか保証しません。別のSP向けに発行されたAssertionや、期限切れのAssertionでも署名は有効だからです。AudienceとRecipientを見ない実装では、同じIdPを使う別のSaaS向けのAssertionを持ち込まれても、署名検証だけは通ります。
IdP主導のログイン(unsolicited response)は例外です。この場合InResponseToを含んではならないと仕様に書かれています。InResponseToの検証を実装するときは、SP起点ログインだけに適用し、IdP起点を受け付けるかどうかを先に決めておく必要があります。
署名ラッピングは「検証した要素」と「読む要素」のずれで起きる
署名ラッピングは、この記事の整理では次の形の問題です。署名付きの本物のAssertionを残したまま、攻撃者が別のAssertionを同じ文書に差し込みます。SPが署名は本物のほうで検証し、ユーザー名は差し込まれた偽のほうから読むと、改ざんが通ります。仕様の規則で言えば「署名を検証する」と「依拠するAssertionが有効か確認する」が別々の操作であるため、両方が同じ要素を指している保証はコード側で作る必要があります。
次は、Claude Codeに点検させたい形の疑似コードです。
# 疑似コード: 署名ラッピングを許す書き方
def handle_acs(saml_response_xml):
doc = parse(saml_response_xml)
if not verify_signature(doc, IDP_CERT): # 署名は文書のどこかを検証
raise AuthError("bad signature")
# 検証した要素とは無関係に、最初に見つかった要素を読む
nameid = doc.find(".//saml:Assertion//saml:NameID").text
create_session(nameid)点検してほしいのは、署名検証が返した「署名済み要素」と、NameIDや属性を読み出す要素が同一かどうかです。直した形は、検証関数が返した署名済みのAssertionだけから値を読みます。
# 疑似コード: 署名済み要素だけから読む書き方
def handle_acs(saml_response_xml, expected_request_id, acs_url, sp_entity_id):
signed = verify_and_return_signed_assertion(saml_response_xml, IDP_CERT)
check_audience(signed, sp_entity_id)
check_recipient_and_expiry(signed, acs_url)
check_in_response_to(signed, expected_request_id)
reject_replay(signed.id, signed.not_on_or_after)
create_session(signed.name_id) # 署名済みの値だけを使うverify_and_return_signed_assertionの中身は、実績のあるSAMLライブラリに任せるのが現実的です。自前でXML署名の検証を書くと、正規化や参照先の解決で別の穴を作りやすいためです。点検の対象は、そのライブラリの戻り値を呼び出し側が正しく使っているかになります。
Claude Codeには読み取りだけを許して点検させる
点検では、Claude Codeにコードを読ませるだけでよく、実行や書き込みは最小限にします。公式ドキュメントによれば、手動モードのClaude Codeは読み取り専用の権限から始まり、編集やコマンド実行の前に確認を求めます。テストの実行だけを許可する構成にすると、点検の範囲で意図しない操作が走りにくくなります。
テスト用の秘密鍵や本番のIdP証明書は、点検に渡す必要がありません。Readの拒否ルールで、ファイルツールからの読み取りを止められます。
{
"permissions": {
"allow": ["Bash(pytest tests/saml *)"],
"deny": ["Read(./.env)", "Read(./secrets/**)"]
}
}拒否ルールは許可ルールより先に評価され、拒否に一致した呼び出しは許可ルールがあっても通りません。ただしReadの拒否ルールが対象にするのは組み込みのファイルツールと、catなど認識されるファイルコマンドです。スクリプトがファイルを自分で開くケースまでは止まらないため、鍵を絶対に読ませたくない場合はサンドボックスの併用が公式の案内です。
観点はCLAUDE.mdに書いておくと、毎回の指示がぶれません。
## SAML SPの点検観点
- 署名検証の戻り値(署名済み要素)だけからNameIDと属性を読むこと
- Audience / Recipient / NotOnOrAfter / InResponseTo を検証する箇所を、
ファイルと行番号で列挙すること
- 使用済みAssertion IDの保存と期限切れ削除があるか確認すること
- コードを直す前に、失敗するべきテストを先に提案すること点検の依頼は、検証項目ごとに「どこで確認しているか、確認していなければその旨」を表で返させる形にします。「問題ありません」の一言で終わる返答は、見落としを確認できないため受け取らないほうが安全です。
改ざんしたレスポンスで必ず落ちるテストを書かせる
コードを読んだ所見は、テストで裏付けて初めて意味を持ちます。有効なレスポンスを1つ用意し、1か所ずつ壊したものが拒否されることを確かめます。壊し方と期待結果は次のとおりです。
| 改ざん | 期待する結果 | 見つかる欠陥 |
|---|---|---|
| 署名値を1文字書き換える | 期待する結果拒否 | 見つかる欠陥署名検証の省略 |
Audienceを別のSPの識別子に変える | 期待する結果拒否 | 見つかる欠陥Audience未検証 |
Recipientを別のURLに変える | 期待する結果拒否 | 見つかる欠陥Recipient未検証 |
NotOnOrAfterを過去にする | 期待する結果拒否 | 見つかる欠陥期限の未検証 |
InResponseToを未知のIDにする | 期待する結果拒否 | 見つかる欠陥リクエストとの対応付けなし |
| 同じレスポンスを2回送る | 期待する結果2回目は拒否 | 見つかる欠陥リプレイ対策なし |
| 本物のAssertionを残し、偽のAssertionを前に挿入 | 期待する結果偽のNameIDでログインできない | 見つかる欠陥署名ラッピング |
署名値の書き換えと、署名済みの内容を変えるケースは分けます。Audienceなどを書き換えると署名も壊れて拒否されるため、署名検証が先に落ちるだけで、Audience検査そのものはテストされません。Audience、Recipient、期限、InResponseToの検査を個別に確かめるには、テスト内でテスト用の鍵を使って改ざん後のAssertionに署名し直す必要があります。この「署名し直す」補助関数をClaude Codeに書かせると、各検査が独立して働くかを確かめられます。
@pytest.mark.parametrize("field,value", [
("audience", "https://other-sp.example/metadata"),
("recipient", "https://evil.example/saml/acs"),
("not_on_or_after", "2000-01-01T00:00:00Z"),
("in_response_to", "_unknown_request_id"),
])
def test_resigned_tampered_assertion_is_rejected(field, value, client):
resp = build_valid_response(client.pending_request_id)
resp = tamper_and_resign(resp, field, value, TEST_IDP_KEY)
r = client.post("/saml/acs", data={"SAMLResponse": b64(resp)})
assert r.status_code in (400, 401, 403)
assert not client.session_exists()実行はClaude Codeに任せ、失敗したテストの出力を読ませて実装を直させます。最初は、現状のコードで「通ってしまうテストが何本あるか」を数えるのが目的です。通ってしまうテストが、そのまま未検証の項目一覧になります。
点検の依頼文とリプレイ対策の見直し方
最初の依頼は、実装を直させずに現状を表にさせる内容にします。
src/auth/saml/ の ACS ハンドラを読み取り専用で点検して。
署名・Audience・Recipient・NotOnOrAfter・InResponseTo・リプレイ対策の
6項目それぞれについて、検証している箇所(ファイル:行)と、
検証していない場合はその旨を表で返して。コードは変更しないこと。返ってきた表で「検証箇所なし」の行が、直す対象です。ここでInResponseToが空欄なら、SP起点ログインで発行したAuthnRequestのIDを保存していない可能性があります。IDの保存先を決めるところから実装が必要になるため、点検の範囲を超えます。その場合は、保存先の設計と修正を別の依頼に分けます。
リプレイ対策は、仕様が「使用済みのID値を、Assertionが有効とみなされる期間のあいだ保持する」と定めています。見るべきは次の2点です。
- 保存するのは
AssertionのIDで、保持期間はNotOnOrAfterまでで足りる - 複数のアプリサーバーがある構成では、プロセス内メモリではなく共有ストアに置く
プロセス内の変数に使用済みIDを持つ実装は、1台構成のテストでは通ります。台数を増やした途端、2回目のPOSTが別のサーバーに届いて通ってしまいます。この種の欠陥はコードの読解だけでは見落としやすく、同じレスポンスを2回送るテストを、負荷分散を挟んだ環境で流して初めて表に出ます。
メタデータ側の宣言も確認の対象です。仕様では、SPがSPSSODescriptorのWantAssertionsSigned属性で、署名付きAssertionを求める意思を示せます。ただしIdPがこれに従う義務はないとも書かれています。メタデータに書いたから安全、ではなく、SP側のコードが署名なしのAssertionを実際に拒否するかをテストで確かめます。
点検結果を鵜呑みにしないための線引き
Claude Codeの点検は、コードレビューの補助です。公式ドキュメントも、提案されたコードやコマンドの安全性を確認する責任は利用者にあると明記しています。次の3点は人が最終確認します。
- 署名検証をライブラリに任せている場合、そのライブラリのバージョンと既知の脆弱性情報
- 鍵のローテーション時に、複数の証明書を受け付ける期間の扱い
- 時計のずれの許容幅。仕様は許容を認めていますが、具体的な値は定めていません
仕様のPDFは2005年の版です。その後に見つかった実装上の落とし穴を網羅しているわけではないため、フレームワーク公式の注意書きと併せて点検項目を更新します。
Webhookの署名検証との違いも押さえておくと混乱しません。HMACで本文全体を検証する方式はManaged Agentsのwebhook署名検証のようにペイロード全体が検証対象です。SAMLは署名がXML文書の一部だけを対象にでき、署名の外側に要素を足せてしまう点が根本的に違います。
セッション期限とクライアントアドレスも拾っておく
ログイン成立後の扱いにも、仕様の規則があります。AuthnStatementにSessionNotOnOrAfterが含まれる場合、SPはその時刻に達したらセキュリティコンテキストを破棄してよい、と書かれています(SHOULD)。自社のセッション有効期限が、この値より長く設定されていないかを点検に加えます。
SubjectConfirmationDataにAddress属性が含まれる場合、SPはユーザーエージェントのアドレスと照合してもよい(MAY)とされています。任意の検査なので、実装していなくても仕様違反ではありません。モバイル回線やプロキシ経由でアドレスが変わる環境では誤拒否の原因になるため、入れるかどうかは運用で決めます。
また、検証に失敗したAssertionは破棄し、ログインの成立に使わないことが仕様の想定です(SHOULD NOT)。点検では「検証失敗のとき、例外を握りつぶして処理を続けていないか」を見ます。ログに失敗理由を残し、利用者には汎用のエラーだけを返す形が無難です。
まとめ
SAML SPの点検は、署名の有無ではなく「署名した要素から値を読んでいるか」と「Audience・Recipient・期限・InResponseTo・リプレイを個別に拒否できるか」で決まります。Claude Codeには読み取り中心の権限で検証箇所を表にさせ、署名し直した改ざんレスポンスのテストで未検証の項目を洗い出します。IdPごとのSSO設定はSSOのIdP比較に整理があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。