CSRF の攻撃タイプ#
CSRF、正式には Cross Site Request Forgery(クロスサイトリクエストフォージェリ)、または XSRF、ワンクリック攻撃、セッションライディングとも呼ばれる、信頼されたユーザーがサーバーに予期しないリクエストを送信することを奪う攻撃手法です。攻撃者は被害者を第三者のウェブサイトに誘導し、そのサイトから攻撃対象のウェブサイトにクロスサイトリクエストを送信します。被害者が攻撃対象のウェブサイトで既に取得した登録証明書を利用し、バックエンドのユーザー認証を回避し、ユーザーを偽装して攻撃対象のウェブサイトで特定の操作を実行することを目的としています。XSS と比較すると、XSS はユーザーが特定のウェブサイトを信頼することを利用し、 CSRF はウェブサイトがユーザーのウェブブラウザを信頼することを利用します。
通常、CSRF 攻撃は攻撃者が被害者の Cookie を利用してサーバーの信頼を騙し、被害者が全く知らないうちに被害者の名義でリクエストを偽造して攻撃サーバーに送信し、権限保護下の操作を未承認で実行することができます。
知乎の李向天の回答には、CSRF に関する生き生きとした説明があります:
防盗システム起動:
母:服を見ていてね
子供:はい泥棒が来た
通常の仕事:
子供:あなたは誰ですか?
泥棒:私は張三です
子供:お母さん、誰かが服を盗んでいます
母:誰?
子供:張三泥棒が捕まった
脆弱性:
子供:あなたは誰ですか?
泥棒:私はあなたを遊ばせるために来ました
子供:お母さん、誰かが服を盗んでいます
母:誰?
子供:あなたを遊ばせるために
母: ...CSRF はユーザーが知らないうちに、その身分を偽装してリクエストを発起させるものです:
泥棒:あなたのお母さんが洗剤を買いに行けと言っています
CSRF の原理は非常にシンプルで、XSS よりも単調に見えます。具体的には、以下の 3 種類の攻撃タイプが含まれます:
- GET タイプの CSRF
- POST タイプの CSRF
- リンクタイプの CSRF
GET タイプの CSRF#
このタイプの攻撃は非常に簡単で、1 つの HTTP リクエストが必要です:
<img src="http://a.com/withdraw?amount=10000&for=hacker" >
被害者がこの img を含むページにアクセスすると、ブラウザは自動的に a.com に HTTP リクエストを送信します。a.com は被害者のログイン情報を含むクロスドメインリクエストを受け取ります。
POST タイプの CSRF#
このタイプの CSRF は通常、自動送信されるフォームを使用します。例えば:
<form action="http://a.com/withdraw" method=POST>
<input type="hidden" name="account" value="airing" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>
このページにアクセスすると、フォームは自動的に送信され、ユーザーが POST 操作を完了したかのようにシミュレートされます。このタイプの CSRF は最初のものと同様にリクエストをシミュレートしているため、バックエンドインターフェースも POST リクエストのみを許可することに安全性を依存することはできません。
リンクタイプの CSRF#
リンクタイプの CSRF はあまり一般的ではなく、他の 2 つのユーザーがページを開くだけで罹患する状況に比べて、ユーザーがリンクをクリックする必要がありますが、本質的には前の 2 つと同じです。このタイプは通常、フォーラムに投稿された画像に悪意のあるリンクを埋め込むか、広告の形式でユーザーを誘導して罹患させます。攻撃者は通常、ユーザーをクリックさせるために誇張した言葉を使います。例えば:
<a href="http://a.com/withdraw.php?amount=1000&for=hacker" taget="_blank">
ドラゴンを屠る宝刀、クリックするだけでプレゼント!
<a/>
以前にユーザーが信頼できるウェブサイト A にログインし、ログイン状態を保持している限り、ユーザーが上記のページに自発的にアクセスすると、攻撃が成功したことになります。
CSRF の防御方法#
CSRF は通常、第三者のウェブサイトから発起され、攻撃されたウェブサイトは攻撃を防ぐことができません。したがって、自サイトの CSRF に対する防御能力を強化することで安全性を高める必要があります。
上記では CSRF の 2 つの特徴について説明しました:
- CSRF は(通常)第三者のドメインで発生します。
- CSRF 攻撃者は Cookie などの情報を取得できず、ただ使用するだけです。
これらの特徴に基づいて、CSRF には以下の 2 つの防御戦略を策定できます:
- 自動防御:不明な外部ドメインからのアクセスを阻止
- 同一オリジン検証
- SameSite Cookie
- 能動的防御:送信時に本ドメインから取得した情報を要求
- 同期トークン
- ダブルクッキー防御
- カスタムヘッダー
自動防御は HTTP プロトコルの固有の特性を利用して自動的に防護し、能動的防御はプログラミング手段を通じて防御を行います。
CSRF 自動防御戦略#
同一オリジン検証#
CSRF はほとんどが第三者のウェブサイトから来るため、外部ドメインや信頼できないドメインからのリクエストを直接禁止します。
HTTP プロトコルでは、各非同期リクエストには 2 つのヘッダーが付随し、発信元のドメイン名を示します:
- Origin ヘッダー <□ />
- Referer ヘッダー <□ />
これら 2 つのヘッダーが信頼できるかどうかを検証することで同一オリジン検証を実現します。しかし、この方法は完全ではなく、Referer の値はブラウザによって提供されます。HTTP プロトコルには明確な要件がありますが、各ブラウザの Referer の具体的な実装には差異があり、ブラウザ自体にセキュリティの脆弱性がないことを保証することはできません。Referer の値を検証する方法は、セキュリティを第三者に依存させることになり、理論的にはあまり安全ではありません。一部の状況では、攻撃者は自分のリクエストの Referer を隠したり、変更したりすることができます。クローラーを書くときも、通常はヘッダーを変更してサーバーの同一オリジン検証を回避します。【基本功】 フロントエンドセキュリティシリーズ第 2 回:CSRF 攻撃を防ぐ方法 | 美団技術チームでは、Referer の信頼性と危険なシナリオについて具体的に分析していますが、ここでは長さの制約から詳しくは述べません。
以上のように、同一オリジン検証は比較的簡単な防止方法であり、ほとんどの CSRF 攻撃を防ぐことができます。しかし、これは完全ではなく、安全性が高い、またはユーザー入力が多いウェブサイトでは、重要なインターフェースに対して追加の防護措置を講じる必要があります。これが次に説明する能動的防御戦略です。
SameSite Cookie#
この問題を根本的に解決するために、Google は草案を起草し、HTTP プロトコルを改善しました。それはSet-Cookie 応答ヘッダーに SameSite 属性を新たに追加することです。この属性は、このクッキーが「同一サイトクッキー」であることを示し、同一サイトクッキーは第一者クッキーとしてのみ使用でき、第三者クッキーとしては使用できません。SameSite には 2 つの属性値があり、それぞれ Strict と Lax です。
- SameSite=Strict:厳格モードで、このクッキーはどんな状況でも第三者クッキーとして使用できないことを示します。
- SameSite=Lax:緩やかなモードで、Strict よりも制限が緩和されています。このリクエストが同期リクエスト(現在のページを変更するか新しいページを開く)であり、かつ GET リクエストである場合、このクッキーは第三者クッキーとして使用できます。
しかし、SameSite Cookie にもいくつかの問題があります:
- SameSite の互換性はあまり良くなく、現時点では新しい Chrome と Firefox のサポートを除いて、Safari や iOS Safari はまだサポートしていません。現時点では普及は難しいようです。
- さらに、SameSite Cookie には致命的な欠陥があり、サブドメインをサポートしていません。例えば、blog.ursb.me に植え付けられた Cookie は、ursb.me の下に植え付けられた SameSite Cookie を使用できません。これにより、私たちのウェブサイトに複数のサブドメインがある場合、SameSite Cookie を使用して主ドメインにユーザーのログイン情報を保存することができません。各サブドメインはユーザーに再度ログインを要求する必要があります。これは実用的ではありません。
CSRF 能動的防御戦略#
CSRF の能動的防御策には以下の 3 つがあります:
- Synchronizer Tokens:応答ページにトークンをレンダリングし、フォーム送信時に隠しフィールドとして送信します。
- Double Cookie Defense:トークンを Cookie に設定し、POST リクエストを送信する際に Cookie を送信し、ヘッダーまたはボディに Cookie 内のトークンを含め、サーバー側で比較検証します。
- Custom Header:特定のヘッダー(例えば
X-Requested-With: XMLHttpRequest
)を持つリクエストを信頼します。この方法は回避可能であるため、Rails や Django などのフレームワークはこの防止方法を放棄しています。
したがって、以下では前述の 2 つの防御方法について主に説明します。
Synchronizer Token#
Synchronizer Token、すなわち同期フォームの CSRF 検証です。CSRF 攻撃が成功する理由は、サーバーが攻撃者から送信されたリクエストをユーザー自身のリクエストと誤認するからです。したがって、すべてのユーリクエストに、CSRF 攻撃者が取得できないトークンを持たせることを要求できます。サーバーはリクエストが正しいトークンを持っているかどうかを検証し、正常なリクエストと攻撃のリクエストを区別し、CSRF 攻撃を防ぐことができます。
具体的には、以下の 3 つのステップに分かれます:
- CSRF トークンをページに出力します。
- ページから送信されるリクエストにこのトークンを含め、通常はフォームフィールドに隠してパラメータとして送信するか、URL の後にクエリとして追加します。
- サーバーはトークンが正しいかどうかを検証します。
ユーザーがクライアントからトークンを取得し、再度サーバーに送信する際、サーバーはトークンの有効性を判断する必要があります。検証プロセスは、まずトークンを復号し、暗号化された文字列とタイムスタンプを比較します。暗号化された文字列が一致し、時間が未経過であれば、このトークンは有効です。このトークンの値は通常、UserID、タイムスタンプ、ランダム数を使用して、暗号化の方法で生成されます。このような暗号化は、リクエストのユーザー、リクエストの時間を検証でき、トークンが簡単に破られないことを保証します。個人的には、プロジェクトで以下の暗号化方法を使用していますので、参考までに:
import md5 from 'md5'
export const MESSAGE = {
OK: {
code: 0,
message: 'リクエスト成功',
},
TOKEN_ERROR: {
code: 403,
message: 'トークン無効',
},
}
const md5Pwd = (password) => {
const salt = 'Airing_is_genius'
return md5(md5(password + salt))
}
export const validate = (res, check, ...params) => {
for (let param of params) {
if (typeof param === 'undefined' || param === null) {
return res.json(MESSAGE.PARAMETER_ERROR)
}
}
if (check) {
const uid = params[0]
const timestamp = params[1]
const token = params[2]
if (token !== md5Pwd(uid.toString() + timestamp.toString() + KEY))
return res.json(MESSAGE.TOKEN_ERROR)
}
}
この方法は、以前の Referer や Origin を検査するよりも安全です。トークンは生成され、セッションに保存され、その後のリクエスト時にセッションからトークンを取り出し、リクエスト内のトークンと比較します。しかし、以下の 2 点に注意が必要です。
- セッション対クッキー:トークンをセッションに保存できる場合は、良い選択です。
- CSRF トークンの更新:CSRF トークンが Cookie に保存されている場合、同じブラウザでユーザーが切り替わると、新しくログインしたユーザーは古いトークン(以前のユーザーが使用していたもの)を使用し続けることになります。これにより一定のセキュリティリスクが生じるため、ユーザーがログインするたびにCSRF トークンを更新する必要があります。
ダブルクッキーディフェンス#
ダブルクッキーディフェンス、つまり二重クッキー検証です。
セッションに CSRF トークンを保存するのは面倒であり、すべてのインターフェースを一般的に処理することができません。したがって、別の防御手段としてダブルクッキーの送信を使用します。CSRF 攻撃者がユーザーの Cookie を取得できない特性を利用して、Ajax およびフォームリクエストに Cookie 内の値を持たせることを要求できます。
- ユーザーがウェブサイトのページにアクセスする際、リクエストドメインにランダムな文字列を含む Cookie を注入します。
- フロントエンドがバックエンドにリクエストを送信する際、Cookie を取り出し、URL のパラメータに追加します。
- バックエンドインターフェースは、Cookie 内のフィールドと URL パラメータ内のフィールドが一致するかどうかを検証し、一致しない場合は拒否します。
この方法は CSRF トークンに比べてはるかに簡単です。前後のインターセプト方法を通じて自動化して実現できます。バックエンドの検証も便利で、リクエスト内のフィールドを比較するだけで済み、トークンの照会や保存を行う必要がありません。しかし、この方法は大規模には適用されておらず、特に大規模なウェブサイトでは深刻な欠陥があります。例えば:
任意のクロスドメインはフロントエンドが Cookie 内のフィールドを取得できないため(サブドメイン間も含む)、ユーザーが私のme.ursb.meにアクセスすると、私のバックエンド API が api.ursb.me にデプロイされているため、me.ursb.meのユーザーは api.ursb.me の Cookie を取得できず、ダブルクッキーディフェンスを完了できません。このため、私たちの Cookie はursb.meの主ドメインに配置され、各サブドメインがアクセスできるようにしています。しかし、ursb.meの下には他の多くのサブアプリケーションもデプロイされており、もし特定のサブドメイン xxx.ursb.me に脆弱性が存在する場合、この xxx.ursb.me には盗むべき情報がないかもしれませんが、攻撃者はursb.meの Cookie を変更することで XSS 攻撃を実行し、改ざんされた Cookie を利用してme.ursb.meに対して CSRF 攻撃を行うことができます。同時に、Cookie の伝送を安全にするために、この防御方法を採用する場合は、全サイトを HTTPS で確保することが最善です。まだ HTTPS に切り替えていない場合、この方法を使用することにはリスクがあります。
以下は、【基本功】 フロントエンドセキュリティシリーズ第 2 回:CSRF 攻撃を防ぐ方法 | 美団技術チームからのダブルトークン検証の要約です:
利点:
- セッションを使用する必要がないため、適用範囲が広く、実施が容易です。
- トークンはクライアントに保存され、サーバーに負担をかけません。
- トークンに比べて実施コストが低く、前後で統一的にインターセプト検証でき、個々のインターフェースやページを追加する必要がありません。
欠点:
- Cookie に追加のフィールドが増えます。
- 他に脆弱性(例えば XSS)がある場合、攻撃者が Cookie を注入できると、この防御方法は無効になります。
- サブドメインの隔離を実現するのが難しいです。
- Cookie の伝送を安全にするために、この防御方法を採用する場合は、全サイトを HTTPS で確保することが最善です。まだ HTTPS に切り替えていない場合、この方法を使用することにもリスクがあります。