クロスサイトリクエストフォージェリー (CSRF)
クロスサイトリクエストフォージェリー (CSRF) 攻撃では、攻撃者はユーザーやブラウザーを欺き、悪意のあるサイトから対象のサイトへ HTTP リクエストを送信させます。このリクエストにはユーザーの資格情報が記載されており、サーバーはユーザーが意図したものと誤認して、何らかの有害な行為を発生させます。
概要
ウェブサイトは通常、ユーザーのブラウザーから HTTP リクエストを受け取ることで、ユーザーに代わって特別な行為(例えば、商品の購入や予約など)を実行します。このリクエストには、実行すべき動作を詳細に指定する引数が含まれていることが多くの場合あります。そのリクエストが本当に当該ユーザーから送信されたものであることを確かめるため、サーバーは、リクエストにユーザーの資格情報が含まれていることを期待します。例えば、ユーザーのセッション ID を含むクッキーなどです。
次の例では、ユーザーはすでに銀行にログインしており、ブラウザーにはそのユーザーのセッションクッキーが保存されています。このページには <form> 要素が含まれており、ユーザーはこの要素を使用して他の人へ送金することができます。ユーザーがフォームを送信すると、ブラウザーはフォームデータを含めた POST リクエストをサーバーに送信します。ユーザーがログインしている場合、リクエストにはユーザーのクッキーが含まれます。サーバーはクッキーを検証し、特別な動作(この場合は送金)を実行します。
このガイドでは、同様に特別な行為を伴うリクエストを、「状態変更リクエスト」と呼ぶことにします。
CSRF 攻撃では、攻撃者はフォームを含むウェブサイトを生成します。そのフォームの action 属性は銀行のウェブサイトに向けられており、フォームには銀行の入力項目を模倣した非表示の入力フィールドが含まれています。
<form action="https://my-bank.example.org/transfer" method="POST">
<input type="hidden" name="recipient" value="attacker" />
<input type="hidden" name="amount" value="1000" />
</form>
このページには、ページの読み込み時にフォームを送信する JavaScript も含まれています。
const form = document.querySelector("form");
form.submit();
ユーザーがページにアクセスすると、ブラウザーは銀行のウェブサイトへフォームを送信します。ユーザーは銀行にログイン済みのため、リクエストにはユーザー本人のクッキーが含まれている可能性があります。そのため、銀行のサーバーはリクエストを正常に検証し、資金を送金します。
攻撃者がクロスサイトリクエストフォージェリー (CSRF) 攻撃を実行する方法はそれ以外にもあります。例えば、ウェブサイトがアクションを実行するために GET リクエストを使用している場合、攻撃者はフォームをまったく使用する必要がなく、次のようなマークアップを含むページへのリンクをユーザーに送信することで攻撃を実行できます。
<img
src="https://my-bank.example.org/transfer?recipient=attacker&amount=1000" />
ユーザーがページを読み込むと、ブラウザーは画像リソースを取得しようと試みますが、これは実に取引リクエストそのものです。
一般的に、以下の条件を満たす場合、ウェブサイトは CSRF 攻撃を受ける可能性があります。
- HTTP リクエストを使用して、サーバー上の状態を変更する。
- リクエストが認証済みのユーザーから送信されたものであることを確認するために、クッキーのみを使用している。
- 攻撃者が予測可能なリクエスト引数のみを使用している。
CSRF に対する防御
この節では、CSRF に対する 3 つの防御策の概要を説明し、さらに、これらいずれかの防御策を補完する多層防御として使用できる 4 つ目の手法についても紹介します。
-
最初の主な防御策は、ページに埋め込まれた CSRF トークンを使用することです。これは、前述の例のように、フォーム要素から状態を変更するリクエストを送信する場合に最も一般的な方法です。
-
2 つ目は、HTTP のフェッチメタデータヘッダーを使用して、状態を変更するリクエストがサイトをまたがって行われているかどうかを調べることです。
-
3 つ目は、状態を変更するリクエストが単純なリクエストではないことを確認し、オリジンを越えるリクエストがデフォルトでブロックされるようにすることです。この方法は、
fetch()のような JavaScript API から状態を変更するリクエストを発行する場合に適しています。
最後に、SameSiteクッキー属性について説明します。これは、前述のいずれかの手法と組み合わせて、多層防御を実現します。
CSRF トークン
この防御策では、サーバーがページを配信する際、CSRF トークンと呼ばれる予測不可能な値をページに埋め込みます。その後、正当なページがサーバーに状態を変更するリクエストを送信する際、その HTTP リクエストに CSRF トークンを含めます。サーバーはトークンの値を調べ、一致する場合にのみリクエストを実行します。攻撃者はトークンの値を推測できないため、偽造に成功することはできません。たとえ攻撃者が使用済みのトークンを発見したとしても、トークンが毎回変更されるのであれば、リクエストを再送信することはできません。
フォーム送信の際、CSRF トークンは通常、非表示のフォームフィールドに記載されています。これにより、フォームが送信されると、トークンは自動的にサーバーに送信され、調べられます。
fetch() のような JavaScript API の場合、トークンはクッキーに保存されるか、ページ内に埋め込まれることがあり、JavaScript がその値を抽出して、追加のヘッダーとして送信します。
現行のウェブフレームワークは通常、CSRF トークンに標準で対応しています。例えば、Django では、csrf_token タグを使用してフォームを保護することができます。これにより、トークンを含む追加の非表示フォームフィールドが生成され、フレームワークがサーバー側でそのトークンを調べます。
この保護機能を活用するには、ウェブサイト内のどの場所で状態を変更する HTTP リクエストを使用しているかをすべて把握し、選択したフレームワークが提供する防御策を確実に実現する必要があります。
フェッチメタデータ
フェッチメタデータとは、HTTP リクエストのコンテキストに関する追加情報を提供する、ブラウザーによって追加される HTTP リクエストヘッダーの集合のことです。サーバーはこれらのヘッダーを使用して、リクエストを許可するかどうかを判断することができます。
CSRF に関して最も重要なのは、Sec-Fetch-Site ヘッダーです。このヘッダーは、このリクエストが同一オリジン、同一サイト、クロスサイト、ユーザーによって直接開始されたもののどれであるかをサーバーに指示します。サーバーはこの情報を使用して、オリジン間リクエストを許可したり、潜在的な CSRF 攻撃としてブロックしたりすることができます。
例えば、この Express のコードでは、同一サイトおよび同一オリジンからのリクエストのみをすることができます。
app.post("/transfer", (req, res) => {
const secFetchSite = req.headers["sec-fetch-site"];
if (secFetchSite === "same-origin" || secFetchSite === "same-site") {
console.log("allowed");
// 状態を更新
} else {
console.log("denied");
// 状態を更新しない
}
});
フェッチメタデータリクエストヘッダーの完全な一覧についてはフェッチメタデータリクエストヘッダーを、この機能を使用する方法についてはフェッチメタデータをご覧ください。
単純リクエストの回避
ウェブブラウザーは、HTTP リクエストを 2 種類に分類します。それは、単純リクエストと、その他のリクエストです。
単純リクエスト、つまり <form> 要素の送信によって行われるようなリクエストは、ブロックされることなくオリジンをまたいで送信できます。ウェブの黎明期からフォームはオリジンをまたいだリクエストを行えるようになっていたため、互換性を保つためには、今後もフォームがオリジンをまたいだリクエストを行えるようにしておくことが重要です。そのため、CSRF トークンの使用など、フォームを CSRF から守るためのその他の対策を実装する必要があるのです。
しかし、ウェブプラットフォームの他の一部、特に fetch() のような JavaScript API は、異なる種類のリクエスト(例えば、独自のヘッダーを設定するリクエストなど)を行うことが可能ですが、これらのリクエストはデフォルトでオリジンまたぐことが許可されていないため、CSRF 攻撃は成功しません。
したがって、fetch() や XMLHttpRequest を使用するウェブサイトは、自身が発行する状態変更するリクエストが単純リクエストにならないようにすることで、CSRF に対する防御策を講じることができます。
例えば、リクエストの Content-Type を "application/json" に設定すると、単純リクエストとして扱われるのを防ぐことができます。
fetch("https://my-bank.example.org/transfer", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ recipient: "joe", amount: "100" }),
});
同様に、リクエストに独自のヘッダーを設定することで、それが単純リクエストとして扱われるのを防ぐことができます。
fetch("https://my-bank.example.org/transfer", {
method: "POST",
headers: {
"X-MY-BANK-ANTI-CSRF": 1,
},
body: JSON.stringify({ recipient: "joe", amount: "100" }),
});
ヘッダー名は、標準ヘッダーと競合しない限り、任意の名前を付けることが可能です。
その後、サーバーはそのヘッダーが存在するか調べることができます。もし存在すれば、サーバーはそのリクエストが単純リクエストとして扱われなかったことを認識します。
単純でないリクエストと CORS
これまで、単純でないリクエストはデフォルトではオリジンをまたいで送信されないと言いました。ただし、オリジン間リソース共有 (CORS) プロトコルにより、ウェブサイトはこの制限を緩和することができます。
仕様上、状態を変更するリクエストに対するレスポンスに以下の内容が含まれる場合、ウェブサイトは特定のオリジンからCSRF攻撃を受ける可能性があります。
Access-Control-Allow-Originというレスポンスヘッダーがあり、このヘッダーには送信元のオリジンが列挙されていますAccess-Control-Allow-Credentialsレスポンスヘッダー。
多層防御: SameSite クッキー
SameSite クッキー属性は、CSRF 攻撃に対するある程度の防御機能を提供します。これは完全な防御策ではありませんが、他の防御策の一つを補完するものとして捉え、多層防御の一環として機能すると考えるのが最適です。
この属性は、ブラウザーがサイト間リクエストにクッキーを記載するタイミングを制御します。この属性には、None、Lax、Strict の 3 つの値があります。
Strict 値は最も強力な保護機能を提供します。この属性が設定されている場合、ブラウザーはサイト間リクエストにおいて、そのクッキーを一切含めません。しかし、これには利便性の課題が生じます。ユーザーがサイトにログインした状態で、別のサイトからリンクをたどってサイトにアクセスした場合、クッキーが含まれないため、ユーザーがサイトに到達しても認識されなくなってしまいます。
Lax 値では、この制限が緩和されます。以下の両方の条件が満たされる場合、サイト間リクエストにクッキーが含まれます。
ただし、Lax は Strict に比べて保護機能がかなり弱くなります。
- 攻撃者は最上位ナビゲーションを発生させる可能性があります。例えば、この記事の冒頭で、攻撃者がターゲットに対してフォームを送信する CSRF 攻撃を表示させましたが、これは最上位ナビゲーションと見なされます。もしそのフォームが
GETメソッドで送信された場合でも、リクエストにはSameSite=Laxを指定したクッキーが含まれたままとなります。 - たとえサーバーが、リクエストが
GETを使って送信されていないかを調べていたとしても、一部のウェブフレームワークは「メソッドオーバーライド」に対応しています。これにより、攻撃者はGETを使ってリクエストを送信しながらも、サーバー側にはPOSTを使ったかのように見せることができます。
したがって、一般的なガイドとして、一部のクッキーには Strict を、その他のクッキーには Lax を使用しましょう。
- ログイン済みのユーザーにページを表示させるかどうかを判断するために使用するクッキーは
Lax - サイト間でのアクセスを許可したくない、状態変更リクエストに使用するクッキーには
Strictを設定してください。
SameSite 属性のもう一つの問題は、異なるサイトからのリクエストに対しては保護しますが、異なるオリジンからのリクエストに対しては保護しないという点です。これは、保護の範囲が狭いと言えます。なぜなら、(例えば)https://foo.example.org と https://bar.example.org は、オリジンが異なっていても、同じサイトと考えてみるからです。実質的に、SameSite による保護に頼っている場合、サイトのすべてのサブドメインを信頼しなければなりません。
SameSite の制限に関する詳細については、SameSite クッキーの制限を回避する方法をご覧ください。
防御の概要チェックリスト
- ウェブサイト内のどこで、リクエストを発行したユーザーを確認するためにセッションクッキーを扱う、状態変更リクエストを実装しているかを把握してください。
- 本文書で記述されている主要な防御策のうち、少なくとも 1 つを実装してください。
<form>要素を使用してこれらのリクエストを発行している場合は、CSRF トークンを対応しているウェブフレームワークで確実に使用し、それを活用してください。fetch()やXMLHttpRequestなどの JavaScript API を使用して状態変更リクエストを発行する場合は、それらが単純リクエストにならないことを確認してください。- どのようなメカニズムを使用してリクエストを発行する場合でも、フェッチメタデータを使用してサイト間リクエストを禁止することを考えてみてください。
- 状態変更リクエストの発行に
GETメソッドを使用することは避けるましょう。 - セッションクッキーの
SameSite属性は、可能であればStrictに、やむを得ない場合はLaxに設定してください。