クロスサイトリクエストフォージェリ(CSRF)を簡単に理解する
以前、クロスオリジンデータ共有であるCORSと、攻撃者がユーザーの個人情報や機密データにアクセスできる可能性について説明しました。そこでの焦点はデータの「読み取り」でした。
さて、CSRF(Cross-Site Request Forgery)と呼ばれる別の同様の攻撃について話しましょう。ここでの重要な違いは、CSRFが「アクションの実行」に焦点を当てていることです。
簡単な例を通して、CSRFとは何かを理解することから始めましょう!
削除機能によるCSRFの紹介
以前、ブログのようなシンプルなバックエンドページを作成したことがあります。記事の作成、削除、編集機能がありました。
削除機能を実装する方法はたくさんありますが、API呼び出しを行ったり、フォームを送信したりするなどです。しかし、私は怠惰だったので、より簡単なアプローチを選びました。
簡単にするために、この機能をGETリクエストにすれば、フロントエンドでほとんどコードを書かずにリンクだけで削除を達成できると考えました。
<a href="/delete?id=3">削除</a>
便利ですよね?そして、バックエンドでは、リクエストにセッションIDが含まれているか、記事がそのIDの作成者によって書かれたものかを確認する検証を追加しました。両方の条件が満たされた場合にのみ、記事が削除されます。
やるべきことはすべてやったように見えます。「作成者本人のみが自分の記事を削除できる」のです。安全なはずですよね?何か見落としはありますか?
確かに、権限チェックは正しく、「作成者本人のみが自分の記事を削除できる」のです。しかし、作成者の知らないうちに他の誰かが記事を削除した場合はどうなるでしょうか?私が何を言っているのかわからないと思うかもしれません。作成者が開始しなかったのに、どうやって記事を削除できるのでしょうか?
さて、それがどのように行われるかをお見せしましょう!
ボブが悪意のある悪役で、ピーターに気づかれずに自分の記事を削除させたいとします。どうすればよいでしょうか?
ピーターが心理テストが大好きであることを知っているボブは、心理テストのウェブサイトを作成し、ピーターに送信します。ただし、このテストウェブサイトには次のようなボタンがあります。
<a href="https://small-min.blog.com/delete?id=3">テストを開始</a>
興奮したピーターは「テストを開始」ボタンをクリックします。クリックすると、ブラウザはhttps://small-min.blog.com/delete?id=3
にGETリクエストを送信し、ブラウザのメカニズムにより、small-min.blog.com
のCookieも一緒に送信します。
サーバーがリクエストを受信すると、セッションを確認し、それが確かにピーターであり、記事が彼によって書かれたものであることを確認します。その結果、サーバーは記事を削除します。
これがCSRF、クロスサイトリクエストフォージェリです。
あなたは明らかに心理テストのウェブサイト(例えばhttps://test.com
)にいますが、知らず知らずのうちにhttps://small-min.blog.com
の記事を削除してしまいました。恐ろしくないですか?超怖いです!
これが、CSRFがワンクリック攻撃とも呼ばれる理由です。ワンクリックで侵害されてしまいます。
観察力の鋭い人は、「でも、ピーターは何が起こったのか気づかないの?ブログにアクセスしたのだから、『知らないうちに』という条件には合わないのでは?」と言うかもしれません。
これらは些細な問題です。このように変更したらどうでしょうか。
<img src="https://small-min.blog.com/delete?id=3" width="0" height="0" />
<a href="/test">テストを開始</a>
ページを開く際に、見えない画像で密かに削除リクエストを送信します。今回は、ピーターは本当に何も知りません。これで条件に合致します!
この簡単な例から、CSRFの原理と攻撃方法が明確にわかります。
CSRF攻撃の目的は、「別のウェブサイトからターゲットのウェブサイトにリクエストを送信し、ターゲットのウェブサイトに、そのリクエストがユーザー自身によって開始されたと誤認させること」です。
これを達成するには、ブラウザのメカニズムに依存します。ウェブサイトにリクエストを送信すると、関連するCookieが含まれます。ユーザーがログインしている場合、リクエストには当然その情報(セッションIDなど)が含まれ、ユーザーがリクエストを開始したかのように見えます。
結局のところ、サーバーは通常、あなたが誰であるかを気にしません。Cookie、より正確にはCookieに含まれる情報のみを認識します。サイトAからサイトBにリクエストを送信すると、サイトBのCookieが含まれます。同様に、サイトCからサイトBにリクエストを送信すると、サイトBのCookieも含まれます。これがCSRFが機能する主な理由です。
しかし、上記の例には欠陥があります。「削除リクエストをPOSTに変更すればよいのでは?」
その通りです!怠けずに、削除機能をPOSTを使用して適切に実装しましょう。こうすれば、<a>
や<img>
を介して攻撃することはできなくなりますよね?除非…POSTリクエストを送信できるHTML要素があるのでしょうか?
あります、それは<form>
と呼ばれます。
<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3" />
<input type="submit" value="テストを開始" />
</form>
ピーターがそれをクリックした後、彼は再び罠にはまり、記事は削除されました。前回は見えない画像を介して、今回はフォームを介してです。
しかし、ピーターはそれに気づかないのだろうかと疑問に思うかもしれません。私も懐疑的だったので、Googleで検索してこの記事を見つけました:Example of silently submitting a POST FORM (CSRF)
記事で提供されている例は次のとおりです。ウェブの世界は本当に広大で深遠です。
<iframe style="display:none" name="csrf-frame"></iframe>
<form
method="POST"
action="https://small-min.blog.com/delete"
target="csrf-frame"
id="csrf-form"
>
<input type="hidden" name="id" value="3" />
<input type="submit" value="submit" />
</form>
<script>
document.getElementById("csrf-form").submit();
</script>
見えないiframeを開き、フォーム送信後の結果がiframe内に表示されるようにします。そして、このフォームはピーターの操作なしに自動的に送信することもできます。
この時点で、POSTに変更しても無駄であり、CSRFの問題は依然として存在することがわかります。
そこで、あなたは賢いアイデアを思いつきます。「フロントエンドではフォームしかPOSTリクエストを送信できないのだから、APIをJSON形式でデータを受け入れるように変更すればいいのでは?そうすればフォームは使えなくなるでしょう!」
HTMLフォームの場合、enctype
は3種類のみサポートしています。
application/x-www-form-urlencoded
multipart/form-data
text/plain
ほとんどの場合、最初のタイプが使用され、ファイルアップロードの場合は2番目のタイプが使用され、3番目のタイプはめったに使用されません。サーバー側でJSONを解析したい場合、通常、コンテンツタイプはapplication/json
になります。
したがって、この記述は半分正しいです。一部のサーバーでは、リクエストのコンテンツタイプがapplication/json
でない場合、エラーをスローし、有効なリクエストとは見なしません。
誤っている部分は、他のサーバーでは、ボディコンテンツがJSON形式である限り、コンテンツタイプがtext/plain
であっても受け入れられるということです。JSON形式のボディは、以下のフォームを使用して構築できます。
<form
action="https://small-min.blog.com/delete"
method="post"
enctype="text/plain"
>
<input name='{"id":3, "ignore_me":"' value='test"}' type="hidden" />
<input type="submit" value="delete!" />
</form>
<form>
はname=value
というルールに従ってリクエストボディを生成するため、上記のフォームは次のリクエストボディを生成します。
{"id":3, "ignore_me":"=test"}
記事の削除の例を使用しましたが、これは重要ではないように思えるかもしれません。しかし、銀行振込の場合はどうでしょうか?攻撃者は自分のウェブページに自分の口座に送金するコードを記述し、このウェブページを配布するだけで、多額の金銭を受け取ることができます。
たくさん話しましたが、それに対する防御方法について話しましょう!最も簡単な防御である「ユーザー」から始めましょう。
ユーザーによる防御
CSRF攻撃が機能するのは、ユーザーが攻撃対象のウェブページに既にログインしているため、特定のアクションを実行できるからです。これらの攻撃はウェブページ側で処理されるべきですが、本当に心配で、ウェブページが適切に処理できないのではないかと懸念している場合は、ウェブサイトの使用後に毎回ログアウトすることでCSRFを回避できます。
しかし、ユーザーができることは実際には非常に限られています。実際の作業はサーバー側で行われるべきです。
サーバー防御
CSRFが危険な理由は、その名前にある「CS」、つまりクロスサイトだからです。どのウェブサイトからでも攻撃を開始できます。CSRFに対する防御は、この観点から考えることができます。簡単に言えば、「他のソースからのリクエストをどのようにブロックできるか?」ということです。
よく考えてみてください。CSRF攻撃のリクエストとユーザーが行ったリクエストの違いは何でしょうか?
違いはオリジンにあり、前者は任意のオリジンから送信され、後者は同じオリジンから送信されます(APIとフロントエンドウェブサイトが同じオリジンにあると仮定します)。バックエンドでこれを区別できれば、どのリクエストを信頼すべきかを判断できます。
あまり一般的ではない防御方法について話しましょう。
RefererまたはOriginヘッダーの確認
リクエストのヘッダーにはreferer
というフィールドが含まれており、リクエストがどこから来たかを示します。このフィールドをチェックして、有効なオリジンであるかどうかを確認できます。そうでない場合は、単に拒否します。
一部のリクエストにはorigin
ヘッダーも含まれており、これは同じことを意味し、リクエストがどこから来たかを示します。
ただし、このチェック方法には注意すべき点が3つあります。まず、場合によっては、リファラーやオリジンがリクエストに含まれていない可能性があり、その場合は何もチェックできません。
第二に、一部のユーザーはリファラー機能を無効にしている可能性があり、その場合、サーバーは正規のユーザーからのリクエストを拒否します。
第三に、有効なオリジンかどうかを判断するコードにバグがないことを保証する必要があります。例えば、
const referer = request.headers.referer;
if (referer.indexOf("small-min.blog.com") > -1) {
// pass
}
上記のコードの問題点に気づきましたか?攻撃者のウェブページがsmall-min.blog.com.attack.com
の場合、チェックはバイパスされます。
したがって、referer
またはorigin
の確認は、あまり効果的な解決策ではありません。
CAPTCHAまたはSMS認証の追加
オンラインバンキングで送金する際と同様に、SMSで送信された認証コードの入力を求められることがよくあります。この追加のチェックを追加することで、CSRF攻撃からの保護を保証できます。別のオプションはCAPTCHAを使用することです。攻撃者はCAPTCHAの答えを知らないため、攻撃を続行できません。
これは包括的な解決策ですが、ユーザーエクスペリエンスに影響を与える可能性があります。ユーザーはコメントを残すたびにCAPTCHAを入力するのが面倒だと感じるかもしれません!
したがって、この保護方法は、銀行振込、パスワード変更、給与明細の表示などの重要な操作に適しています。これらの場合、追加の検証レイヤーを実装する必要があります。「SMS認証コード(または電子メール)の受信」という方法は、CSRF攻撃を防ぐだけでなく、XSSからも保護します。ハッカーがページ上でコードを実行できたとしても、携帯電話や電子メールから認証コードを取得することはできません。認証コードを知らなければ、それ以上の操作はできません。
一般的な防御方法
CSRFトークンの追加
CSRF攻撃を防ぐには、特定の情報が「ウェブサイトのみに知られている」ことを保証するだけで済みます。これをどのように達成できるでしょうか?
フォーム内にcsrf_token
という名前の隠しフィールドを追加します。このフィールドの値はサーバーによってランダムに生成され、フォーム送信ごとに一意である必要があります。トークンはサーバーのセッションデータに保存されます。
<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3" />
<input type="hidden" name="csrf_token" value="fj1iro2jro12ijoi1" />
<input type="submit" value="削除" />
</form>
フォームを送信した後、サーバーはフォーム内のcsrf_token
をセッションデータに保存されているものと比較します。それらが一致する場合、リクエストはウェブサイト自体によって送信されたことを意味します。
なぜこれが機能するのでしょうか?攻撃者はcsrf_token
の値を知らず、推測することもできないからです。したがって、正しい値を提供できず、サーバーのチェックが失敗し、操作がブロックされます。
次に、別の解決策を見てみましょう。
ダブルサブミットクッキー
以前の解決策ではサーバー側の状態が必要でした。つまり、CSRFトークンをサーバーに保存してその正当性を検証する必要がありました。この新しい解決策の利点は、サーバーに何も保存する必要がないことです。
この解決策の前半は以前のものと似ています。サーバーはランダムなトークンを生成し、それをフォームに追加します。ただし、この値をセッションに保存する代わりに、同じトークン値を持つcsrf_token
という名前のCookieが設定されます。
Set-Cookie: csrf_token=fj1iro2jro12ijoi1
<form action="https://small-min.blog.com/delete" method="POST">
<input type="hidden" name="id" value="3" />
<input type="hidden" name="csrf_token" value="fj1iro2jro12ijoi1" />
<input type="submit" value="削除" />
</form>
前述の通り、CSRF防御の核心は「悪意のあるリクエストと正当なリクエストを区別すること」です。ダブルサブミットクッキーソリューションはこの考えに基づいています。
ユーザーがフォームを送信すると、サーバーはクッキー内のcsrf_token
とフォーム内のcsrf_token
を比較し、値があるかどうか、等しいかどうかを確認します。これにより、サーバーはリクエストがウェブサイトによって送信されたかどうかを判断できます。
なぜこれが機能するのでしょうか?
攻撃者が攻撃を開始したいとします。前述のCSRFの原理によれば、Cookie内のcsrf_token
がサーバーに送信されます。しかし、フォーム内のcsrf_token
はどうでしょうか?攻撃者は別のオリジンからターゲットウェブサイトのCookieを見ることも、フォームの内容を見ることもできません。したがって、正しい値を知りません。
フォームとCookieのcsrf_token
が一致しない場合、攻撃はブロックされます。
ただし、この方法には欠点があり、後で説明します。
純粋なフロントエンドのダブルサブミットクッキー
フロントエンドについて特に言及する理由は、以前にシングルページアプリケーション(SPA)であるプロジェクトに遭遇したことがあるからです。オンラインで検索すると、「SPAはどのようにCSRFトークンを取得できるのか?」という質問をしている人が見つかります。サーバーはこのためにAPIを提供する必要があるのでしょうか?少し奇妙に思えます。
しかし、この問題を解決するためにダブルサブミットクッキーの精神を利用することができます。この問題を解決する鍵は、サーバーAPIとのやり取りなしにフロントエンドにCSRFトークンを生成させることです。
残りのプロセスは同じです。トークンを生成し、フォームに追加し、Cookieに書き込みます。
なぜフロントエンドがこのトークンを生成できるのでしょうか?このトークン自体の目的には情報が含まれていないからです。攻撃者がそれを推測するのを防ぐためだけです。したがって、フロントエンドまたはバックエンドのどちらで生成されても、推測されない限り、同じ目的を果たします。
ダブルサブミットクッキーの核心的な概念は、「攻撃者はターゲットウェブサイトのクッキーを読み書きできないため、リクエスト内のトークンはクッキー内のトークンとは異なる」ということです。この条件が満たされる限り、攻撃はブロックできます。
その他の解決策
認証にCookieを使用しない
CSRF攻撃は、ブラウザがリクエストに自動的にCookieを含めること、特に認証に使用されるCookieに依存しています。
したがって、認証にCookieを使用しなければ、CSRFの問題は発生しません。
最近の多くのウェブサイトでは、フロントエンドとバックエンドを分離したアーキテクチャを採用しており、フロントエンドは静的なウェブサイトであり、バックエンドはAPIを介してデータのみを提供します。フロントエンドとバックエンドのドメインはしばしば分離されており、例えば、フロントエンドはhttps://huli.tw
、バックエンドはhttps://api.huli.tw
などです。
このアーキテクチャでは、従来のCookieベースの認証の代わりに、多くのウェブサイトがHTTPヘッダーとともにJWT(JSON Web Token)を使用することを選択しています。認証トークンはブラウザのlocalStorageに保存され、Authorization
ヘッダーでバックエンドに送信されます。次のようになります。
GET /me HTTP/1.1
Host: api.huli.tw
Authorization: Bearer {JWT_TOKEN}
この種の認証方法はCookieの使用を完全に回避するため、CSRF攻撃に対して免疫があります。これは防御メカニズムというよりも技術的な選択です。この認証方法を選択する多くの人々は、それがCSRFも防ぐことを認識していないと私は信じています。
ただし、他の欠点もあります。例えば、CookieはHttpOnly
属性で設定してブラウザが直接アクセスできないようにすることができ、攻撃者がトークンを盗むのを困難にします。しかし、localStorage
には同様のメカニズムがありません。XSS攻撃によって侵害されると、攻撃者は簡単にトークンを盗むことができます。
XSSに対する第3の防御線に関する以前の議論でトークンストレージについて説明したので、ここでは再度触れません。
カスタムヘッダーの追加
CSRF攻撃について議論する際、使用される例は通常フォームと画像であり、これらはリクエストにHTTPヘッダーを含めることができません。したがって、フロントエンドからAPI呼び出しを行う際に、X-Version: web
のようなカスタムヘッダーを含めることで、バックエンドがこのヘッダーの有無に基づいてリクエストが正当であるかどうかを識別できるようにすることができます。
これは問題ないように聞こえるかもしれませんが、前述のようにCORS設定に注意する必要があります。
フォームや画像に加えて、攻撃者はfetch
を使用して、次のようなカスタムヘッダーを持つクロスサイトリクエストを直接送信することもできます。
fetch(target, {
method: "POST",
headers: {
"X-Version": "web",
},
});
ただし、カスタムヘッダーを持つリクエストは非シンプルリクエストと見なされ、実際に送信される前にプリフライトリクエストチェックを通過する必要があります。したがって、サーバー側のCORS実装が正しければ、この防御メカニズムは正常に機能します。
しかし、CORS設定に問題がある場合はどうでしょうか?その場合、CSRF攻撃から防御することはできません。
実際の事例
最初に紹介する事例は、Obmi氏が発見した2022年のGoogle Cloud ShellにおけるCSRF脆弱性です。ファイルアップロード用のAPIにCSRF保護が一切施されておらず、攻撃者はこの脆弱性を悪用して~/.bash_profile
のようなファイルをアップロードし、ユーザーがbashを実行するたびにアップロードされたコマンドが実行されるようにすることができました。
全文は以下を参照してください:[ GCP 2022 ] Few bugs in the google cloud shell
2番目のケースは、2023年にErmeticというサイバーセキュリティ企業がAzure Webサービスで発見した脆弱性です。これは非常に興味深いものです。
Azure WebサービスはHerokuに似ており、コードを準備すればWebアプリケーションをデプロイできます。これらのサーバーには、デフォルトでKudu SCMもインストールされており、環境変数や設定の表示、ログのダウンロードなどができますが、アクセスするには認証が必要です。
ここで話す脆弱性はKudu SCMで見つかりました。Kudu SCM APIはCSRFトークンを使用せず、代わりに前述のようにOriginヘッダーをチェックしてリクエストを検証します。
サーバーのURLがhttps://huli.scm.azurewebsites.net
であると仮定すると、以下のオリジンはエラーを返します。
https://huli.scm.azurewebsites.net.attacker.com
(末尾に追加)https://attacker.huli.scm.azurewebsites.net
(先頭に追加)http://huli.scm.azurewebsites.net
(HTTPに変更)
絶望的に見えるかもしれませんが、彼らは特定の位置に_
と-
以外の文字を追加することで、この制限を回避できることを発見しました。
例えば、https://huli.scm.azurewebsites.net$.attacker.com
はチェックを通過できます。
しかし、問題は、これらの特殊文字がブラウザにとって有効なドメイン名ではないことです。では、どうすればよいでしょうか?
彼らは_
をサブドメイン名として使用できることを発見し、次のようなURLを構築できました。
https://huli.scm.azurewebsites.net._.attacker.com
このURLを使用すると、サーバーのオリジンチェックを回避できます(サーバーの正規表現が不適切に記述されていたため)。チェックを回避した後、悪用できるAPIを探し始め、サーバーに圧縮ファイルを直接デプロイできる/api/zipdeploy
というAPIを見つけました!
したがって、このCSRF脆弱性を介して、攻撃者はユーザーのAzure Webサービスにコードをデプロイし、RCEを達成できます。攻撃は、APIを呼び出すHTMLを準備し、それをhttps://huli.scm.azurewebsites.net._.attacker.com
でホストし、ターゲットに送信することで構成されます。
ターゲットがログインしていてリンクをクリックすると、侵害されます。
彼らはこの攻撃をEmojiDeployと呼んでいます。なぜなら、バイパスされたURLの一部である._.
が絵文字のように見え、かわいらしい響きだからです。
ここではいくつかの詳細を省略しましたが、全文は以下で読むことができます:EmojiDeploy: Smile! Your Azure web service just got RCE’d ._.
脆弱性の連鎖:CSRFとセルフXSS
以前XSSについて言及した際、セルフXSSと呼ばれるタイプを紹介しました。これは自分自身にのみ影響するXSSを指します。
例えば、電話番号フィールドにXSS脆弱性があるが、電話番号は自分の個人設定ページでのみ表示され、他の人には表示されない場合、自分で電話番号をXSSペイロードに変更しない限り、攻撃を開始することはできません。
これをCSRFと組み合わせる絶好の機会だと思いませんか?
個人設定ページにCSRF脆弱性があると仮定すると、攻撃者はCSRFを使用して被害者の電話番号をXSSペイロードに変更し、その後個人設定ページを開くことができます。これにより、セルフXSSが実際のXSSに変わります!
元のセルフXSS脆弱性はほとんど影響がなく、多くのバグバウンティプラットフォームでは受け入れられない可能性があります。しかし、CSRFと組み合わせると、本当に影響力のあるXSSになり、深刻度が増し、プラットフォームはそれを受け入れます。
実際の事例として、2016年に@fin1te氏がUberに報告した脆弱性があります:Uber Bug Bounty: Turning Self-XSS into Good-XSS。少し古いですが、そこで議論されているテクニックは依然として非常に実用的です。
彼はpartners.uber.com
でセルフXSSを発見し、それをログアウトCSRFと組み合わせ、現在のユーザーをpartners.uber.com
ドメインでログアウトさせ、login.uber.com
ドメインではログイン状態を維持しました。
その後、ログインCSRFを使用して事前に準備したアカウントにログインし、ログイン後にXSSをトリガーしました。この時点で、iframeを使用してユーザーを再度ログインさせ、このXSSを利用して現在のユーザーのデータにアクセスできるようにしました。これにより、これらの脆弱性が巧みに連鎖され、より大きな影響が生み出されました。
プロセスはやや複雑ですが、これらの脆弱性の連携は非常に興味深く、CSPを使用してページのリダイレクトを防ぐというのも斬新なアプローチです。
まとめ
サイバーセキュリティの世界における脆弱性は相互に関連しています。ある脆弱性を修正する方法を選択する際には、他の脆弱性への影響にも注意することが重要です。
例えば、「認証にCookieを使用しない」はCSRFの問題を解決できますが、XSSがトークンを盗むことを可能にし、XSSの影響範囲を拡大します。一方、「カスタムヘッダーを追加する」はCSRFを防御するように見えるかもしれませんが、CORSが誤って設定されている場合、この防御メカニズムは効果がありません。
したがって、「CSRFトークンを追加する」はより優れた、より一般的なアプローチです。実際、サイバーセキュリティの防御は単一の方法に限定されません。上記で述べたいくつかの方法を組み合わせることができます。
例えば、CSSインジェクションの際にHackMDの事例を挙げました。CSRFトークンを取得したにもかかわらず、サーバーがOrigin
ヘッダーを検証することで第2層の保護を実装していたため、攻撃を開始できませんでした。
一方、前述のEmojiDeployは反例です。彼らはOrigin
ヘッダーのみを検証し、それを誤って実装したため、攻撃を受けました。CSRFトークン保護を追加していれば、攻撃を防ぐことができたでしょう。
参考文献: