クロスサイトリクエストフォージェリ (CSRF)
Spring は、クロスサイトリクエストフォージェリ (CSRF) [Wikipedia] 攻撃から保護するための包括的なサポートを提供します。次のセクションでは、以下について説明します。
CSRF 攻撃とは何ですか?
CSRF 攻撃を理解する最良の方法は、具体例を見ることです。
銀行の Web サイトが、現在ログインしているユーザーから別の銀行口座に送金できるフォームを提供していると仮定します。例: 転送フォームは次のようになります。
<form method="post"
action="/transfer">
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="text"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
対応する HTTP リクエストは次のようになります。
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876
次に、銀行の Web サイトに認証したふりをして、ログアウトせずに悪の Web サイトにアクセスします。邪悪な Web サイトには、次の形式の HTML ページが含まれています。
<form method="post"
action="https://bank.example.com/transfer">
<input type="hidden"
name="amount"
value="100.00"/>
<input type="hidden"
name="routingNumber"
value="evilsRoutingNumber"/>
<input type="hidden"
name="account"
value="evilsAccountNumber"/>
<input type="submit"
value="Win Money!"/>
</form>
お金を獲得したいため、送信ボタンをクリックします。このプロセスでは、意図せずに悪意のあるユーザーに 100 ドルを送金しました。これは、悪の Web サイトがあなたの Cookie を見ることができない一方で、銀行に関連付けられた Cookie がリクエストとともに送信されるためです。
さらに悪いことに、このプロセス全体は JavaScript を使用して自動化できた可能性があります。これは、ボタンをクリックする必要さえなかったことを意味します。さらに、XSS 攻撃 [Wikipedia] の被害者である正直なサイトにアクセスした場合も同様に簡単に発生する可能性があります。では、このような攻撃からユーザーをどのように保護するのでしょうか。
CSRF 攻撃からの保護
CSRF 攻撃が可能である理由は、被害者の Web サイトからの HTTP リクエストと攻撃者の Web サイトからのリクエストがまったく同じであるためです。これは、邪悪な Web サイトからのリクエストを拒否し、銀行の Web サイトからのリクエストのみを許可する方法がないことを意味します。CSRF 攻撃から保護するには、2 つのリクエストを区別できるように、悪意のあるサイトが提供できないリクエストに何かがあることを確認する必要があります。
Spring は、CSRF 攻撃から保護するための 2 つのメカニズムを提供します。
セッション Cookie で SameSite 属性を指定する
どちらの保護にも、安全なメソッドは読み取り専用にするが必要です。 |
安全なメソッドは読み取り専用である必要がある
CSRF に対するいずれかの保護が機能するためには、アプリケーションは「安全な」HTTP メソッドは読み取り専用です [Mozilla] を確認する必要があります。これは、HTTP GET
、HEAD
、OPTIONS
、TRACE
メソッドを使用したリクエストがアプリケーションの状態を変更してはならないことを意味します。
シンクロナイザートークンパターン
CSRF 攻撃から保護するための主で最も包括的な方法は、シンクロナイザートークンパターン [OWASP] (英語) を使用することです。この解決策は、各 HTTP リクエストで、セッション Cookie に加えて、CSRF トークンと呼ばれる安全でランダムに生成された値が HTTP リクエストに存在することを確認することです。
HTTP リクエストが送信されると、サーバーは予想される CSRF トークンを検索し、HTTP リクエストの実際の CSRF トークンと比較する必要があります。値が一致しない場合、HTTP リクエストは拒否されます。
この動作の鍵は、実際の CSRF トークンが、ブラウザーによって自動的に含まれない HTTP リクエストの一部にある必要があることです。例: HTTP パラメーターまたは HTTP ヘッダーに実際の CSRF トークンをリクエストすると、CSRF 攻撃から保護されます。Cookie はブラウザーによって HTTP リクエストに自動的に含まれるため、Cookie で実際の CSRF トークンをリクエストすることは機能しません。
アプリケーションの状態を更新する HTTP リクエストごとに実際の CSRF トークンのみが必要になるという期待を緩和できます。これが機能するには、アプリケーションで安全な HTTP メソッドが読み取り専用であることを確認する必要があります。外部サイトから当社 Web サイトへのリンクを許可するため、これによりユーザビリティが向上します。さらに、トークンが漏洩する可能性があるため、HTTP GET にランダムトークンを含めたくありません。
シンクロナイザートークンパターンを使用すると、例がどのように変化するかを考えてみてください。実際の CSRF トークンは、_csrf
という名前の HTTP パラメーターに含まれている必要があると想定します。アプリケーションの転送フォームは次のようになります。
<form method="post"
action="/transfer">
<input type="hidden"
name="_csrf"
value="4bfd1575-3ad1-4d21-96c7-4ef2d9f86721"/>
<input type="text"
name="amount"/>
<input type="text"
name="routingNumber"/>
<input type="hidden"
name="account"/>
<input type="submit"
value="Transfer"/>
</form>
フォームには、CSRF トークンの値を持つ非表示の入力が含まれています。外部サイトは CSRF トークンを読み取ることができません。同じ発信元ポリシーにより、悪サイトはレスポンスを読み取れないためです。
送金に対応する HTTP リクエストは次のようになります。
POST /transfer HTTP/1.1
Host: bank.example.com
Cookie: JSESSIONID=randomid
Content-Type: application/x-www-form-urlencoded
amount=100.00&routingNumber=1234&account=9876&_csrf=4bfd1575-3ad1-4d21-96c7-4ef2d9f86721
HTTP リクエストに、安全なランダム値を持つ _csrf
パラメーターが含まれていることに気付くでしょう。悪の Web サイトは、_csrf
パラメーター(悪の Web サイトで明示的に提供する必要があります)に正しい値を提供できず、サーバーが実際の CSRF トークンと予想される CSRF トークンを比較すると、転送は失敗します。
SameSite 属性
CSRF 攻撃から保護する新しい方法は、Cookie で SameSite 属性 [Mozilla] を指定することです。サーバーは、Cookie を設定するときに SameSite
属性を指定して、外部サイトから来るときに Cookie を送信しないように指定できます。
Spring Security は、セッション Cookie の作成を直接制御しないため、SameSite 属性のサポートを提供しません。Spring Session は、サーブレットベースのアプリケーションで |
SameSite
属性を持つ HTTP レスポンスヘッダーの例は次のようになります。
Set-Cookie: JSESSIONID=randomid; Domain=bank.example.com; Secure; HttpOnly; SameSite=Lax
SameSite
属性の有効な値は次のとおりです。
Strict
: 指定した場合、同じサイト [Mozilla] からのリクエストには Cookie が含まれます。それ以外の場合、Cookie は HTTP リクエストに含まれません。Lax
: 指定すると、同じサイト [Mozilla] から送信された場合、またはリクエストがトップレベルのナビゲーションから送信され、メソッドが読み取り専用の場合に Cookie が送信されます。それ以外の場合、Cookie は HTTP リクエストに含まれません。
SameSite
属性を使用してこの例を保護する方法を検討してください。銀行のアプリケーションは、セッション Cookie に SameSite
属性を指定することにより、CSRF から保護できます。
セッション Cookie に SameSite
属性が設定されている場合、ブラウザーは引き続き JSESSIONID
Cookie を送信し、銀行の Web サイトからのリクエストを送信します。ただし、ブラウザーは、悪意のある Web サイトからの転送リクエストで JSESSIONID
Cookie を送信しなくなりました。悪意のある Web サイトからの転送リクエストにはセッションが存在しないため、アプリケーションは CSRF 攻撃から保護されます。
CSRF 攻撃から保護するために SameSite
属性を使用する場合は、注意すべき重要な考慮事項 [Mozilla] がいくつかあります。
SameSite
属性を Strict
に設定すると、より強力な防御が提供されますが、ユーザーを混乱させる可能性があります。social.example.com (英語) でホストされているソーシャルメディアサイトにログインしたままのユーザーを考えてみます。ユーザーは、ソーシャルメディアサイトへのリンクを含むメールを email.example.org (英語) で受信します。ユーザーがリンクをクリックすると、ソーシャルメディアサイトで認証されることを当然期待します。ただし、SameSite
属性が Strict
の場合、Cookie は送信されないため、ユーザーは認証されません。
もう 1 つの明らかな考慮事項は、SameSite
属性がユーザーを保護するために、ブラウザーが SameSite
属性をサポートする必要があるということです。最近のほとんどのブラウザーは SameSite 属性をサポートしています [Mozilla] 。ただし、まだ使用されている古いブラウザーは使用できない場合があります。
このため、CSRF 攻撃に対する唯一の保護ではなく、多層防御として SameSite
属性を使用することをお勧めします。
CSRF 保護を使用する場合
CSRF 保護をいつ使用する必要がありますか? 通常のユーザーがブラウザーで処理できるリクエストには、CSRF 保護を使用することをお勧めします。ブラウザー以外のクライアントのみが使用するサービスを作成している場合は、CSRF 保護を無効にすることをお勧めします。
CSRF 保護と JSON
よくある質問は、「JavaScript によって作成された JSON リクエストを保護する必要がありますか?」です。簡単な答えは次のとおりです。それは異なります。ただし、JSON リクエストに影響を与える可能性のある CSRF エクスプロイトがあるため、十分に注意する必要があります。例: 悪意のあるユーザーが次のフォームを使用した JSON を使用した CSRF (英語) を作成する可能性があります:
<form action="https://bank.example.com/transfer" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
これにより、次の JSON 構造が生成されます
{ "amount": 100,
"routingNumber": "evilsRoutingNumber",
"account": "evilsAccountNumber",
"ignore_me": "=test"
}
アプリケーションが Content-Type
ヘッダーを検証していなかった場合、このエクスプロイトにさらされることになります。設定によっては、次のように、URL サフィックスを .json
で終わるように更新することで、Content-Type を検証する Spring MVC アプリケーションを悪用することができます。
<form action="https://bank.example.com/transfer.json" method="post" enctype="text/plain">
<input name='{"amount":100,"routingNumber":"evilsRoutingNumber","account":"evilsAccountNumber", "ignore_me":"' value='test"}' type='hidden'>
<input type="submit"
value="Win Money!"/>
</form>
CSRF およびステートレスブラウザーアプリケーション
アプリケーションがステートレスの場合はどうなるでしょうか? それは必ずしも保護されていることを意味するわけではありません。実際、ユーザーが特定のリクエストに対して Web ブラウザーでアクションを実行する必要がない場合でも、CSRF 攻撃に対して脆弱である可能性があります。
例: 認証のために(JSESSIONID の代わりに)すべての状態を含むカスタム Cookie を使用するアプリケーションについて考えてみます。CSRF 攻撃が行われると、カスタム Cookie は、前の例で JSESSIONIDCookie が送信されたのと同じ方法でリクエストとともに送信されます。このアプリケーションは CSRF 攻撃に対して脆弱です。
基本認証を使用するアプリケーションも、CSRF 攻撃に対して脆弱です。前の例で JSESSIONIDCookie が送信されたのと同じ方法で、ブラウザーがすべてのリクエストにユーザー名とパスワードを自動的に含めるため、アプリケーションは脆弱です。
CSRF の考慮事項
CSRF 攻撃に対する保護を実装する際に考慮すべき特別な考慮事項がいくつかあります。
ログイン
ログインリクエストの偽造 [Wikipedia] から保護するには、ログイン HTTP リクエストを CSRF 攻撃から保護する必要があります。悪意のあるユーザーが被害者の機密情報を読み取れないように、ログインリクエストの偽造から保護する必要があります。攻撃は次のように実行されます。
悪意のあるユーザーは、悪意のあるユーザーの資格情報を使用して CSRF ログインを実行します。これで、被害者は悪意のあるユーザーとして認証されます。
次に、悪意のあるユーザーが被害者をだまして、侵害された Web サイトにアクセスし、機密情報を入力させます。
情報は悪意のあるユーザーのアカウントに関連付けられているため、悪意のあるユーザーは自分の資格情報を使用してログインし、被害者の機密情報を表示できます。
ログイン HTTP リクエストを CSRF 攻撃から確実に保護するために考えられる問題は、ユーザーがセッションタイムアウトを経験して、リクエストが拒否される可能性があることです。セッションタイムアウトは、ログインするためにセッションが必要であるとは思わないユーザーにとっては驚くべきことです。詳細については、CSRF およびセッションタイムアウトを参照してください。
ログアウト
ログアウトリクエストの偽造から保護するには、ログアウト HTTP リクエストを CSRF 攻撃から保護する必要があります。悪意のあるユーザーが被害者の機密情報を読み取れないように、ログアウトリクエストの偽造から保護する必要があります。攻撃の詳細については、このブログ投稿 (英語) を参照してください。
ログアウト HTTP リクエストを CSRF 攻撃から確実に保護するために考えられる問題は、ユーザーがセッションタイムアウトを経験して、リクエストが拒否される可能性があることです。セッションのタイムアウトは、ログアウトするセッションがあることを期待していないユーザーにとっては驚くべきことです。詳細については、CSRF およびセッションタイムアウトを参照してください。
CSRF およびセッションタイムアウト
多くの場合、予想される CSRF トークンはセッションに保存されます。これは、セッションが期限切れになるとすぐに、サーバーは予期された CSRF トークンを検出せず、HTTP リクエストを拒否することを意味します。タイムアウトを解決するためのオプションがいくつかあります(それぞれにトレードオフがあります)。
タイムアウトを軽減する最善の方法は、JavaScript を使用してフォーム送信時に CSRF トークンをリクエストすることです。その後、フォームは CSRF トークンで更新され、送信されます。
別のオプションは、セッションが期限切れになることをユーザーに知らせる JavaScript を用意することです。ユーザーはボタンをクリックしてセッションを続行し、リフレッシュできます。
最後に、予想される CSRF トークンを Cookie に保存できます。これにより、予想される CSRF トークンがセッションよりも長く存続します。
期待される CSRF トークンがデフォルトで Cookie に保存されない理由を確認する人もいるかもしれません。これは、ヘッダー(たとえば、Cookie を指定するため)が別のドメインによって設定される可能性がある既知のエクスプロイトがあるためです。これは、Rails 上の Ruby が、ヘッダー X-Requested-With が存在する場合に CSRF チェックをスキップしなくなっ (英語) たのと同じ理由です。エクスプロイトの実行メソッドの詳細については、この webappsec.org スレッド (英語) を参照してください。もう 1 つの欠点は、状態(つまり、タイムアウト)を削除することにより、トークンが危険にさらされた場合にトークンを強制的に無効にすることができなくなることです。
マルチパート (ファイルアップロード)
マルチパートリクエスト(ファイルのアップロード)を CSRF 攻撃から保護すると、鶏が先か卵が先かという [Wikipedia] 問題が発生します。CSRF 攻撃の発生を防ぐには、HTTP リクエストの本文を読み取って、実際の CSRF トークンを取得する必要があります。ただし、本文を読み取ることは、ファイルがアップロードされることを意味します。つまり、外部サイトがファイルをアップロードできることを意味します。
multipart/form-data で CSRF 保護を使用するには 2 つのオプションがあります。
各オプションにはトレードオフがあります。
Spring Security の CSRF 保護をマルチパートファイルアップロードと統合する前に、まず CSRF 保護なしでアップロードできることを確認する必要があります。Spring でのマルチパートフォームの使用の詳細については、Spring リファレンスの 1.1.11. マルチパートリゾルバーセクションおよび |
CSRF トークンを本文に配置する
最初のオプションは、リクエストの本文に実際の CSRF トークンを含めることです。CSRF トークンを本文に配置することにより、認証が実行される前に本文が読み取られます。これは、誰でもサーバーに一時ファイルを配置できることを意味します。ただし、アプリケーションによって処理されるファイルを送信できるのは、認可されたユーザーのみです。一時ファイルのアップロードによるほとんどのサーバーへの影響はごくわずかであるため、一般的に、これが推奨されるアプローチです。
URL に CSRF トークンを含める
認可されていないユーザーに一時ファイルをアップロードさせることが受け入れられない場合は、フォームのアクション属性にクエリパラメーターとして期待される CSRF トークンを含めることもできます。このアプローチの欠点は、クエリパラメーターがリークされる可能性があることです。より一般的には、機密データが漏洩しないように、本文またはヘッダー内に機密データを配置することがベストプラクティスと見なされます。URI の機密情報をエンコードする RFC 2616 セクション 15.1.3 [W3C] (英語) で追加情報を見つけることができます。
HiddenHttpMethodFilter
一部のアプリケーションでは、フォームパラメーターを使用して HTTP メソッドをオーバーライドできます。例: 次の形式では、HTTP メソッドを post
ではなく delete
として扱うことができます。
<form action="/process"
method="post">
<!-- ... -->
<input type="hidden"
name="_method"
value="delete"/>
</form>
HTTP メソッドのオーバーライドは、フィルターで発生します。そのフィルターは、Spring Security のサポートの前に配置する必要があります。オーバーライドは post
でのみ発生するため、実際に問題が発生する可能性は低いことに注意してください。ただし、Spring Security のフィルターの前に配置されていることを確認することをお勧めします。