

認証とセッション⑦
ログイン済みでも、意図しない操作を防ぐには?
前回は、C#とASP.NET Core Identityでログインからログアウトまでを実装し、ブラウザーに保存されたCookieも確認しました。
有効な認証Cookieを検証できれば、サーバーはその要求をログイン済みユーザーのものとして扱えます。しかし、ここには別の問いがあります。
その要求は、本人が望んで送ったものなのでしょうか。
今回は、別のサイトから利用者のブラウザーに意図しない要求を送らせる「CSRF」を取り上げます。対策がない場合に何が起きるのかを確認し、その後で、⑥のコードに入っている対策を読み解きます。
⑥のアプリが、そのまま攻撃されるという話ではありません。⑥では、Cookieの送信制限と偽造防止トークンの検証を組み合わせています。
対策がないと、何が起こるのか

まず、認証Cookieが付いて要求が届き、サーバーに有効なCSRF対策がない場合を考えます。
ユーザーAは、業務アプリにログインし、作業一覧を時間順で表示しています。そのまま別のタブでメールを開き、「便利な作業テンプレートはこちら」というリンクを押しました。
リンク先は、攻撃者が用意した案内ページでした。このページには、利用者のブラウザーから業務アプリへ「並び順を名前順に変更する」という要求を送らせる仕掛けがあります。
その要求に業務アプリの有効な認証Cookieが付けば、サーバーはユーザーAとして認証できます。ユーザーAに設定変更の権限があり、それ以上の検証をしていなければ、変更を受け付けてしまう可能性があります。
これが、CSRF(Cross-Site Request Forgery/クロスサイトリクエストフォージェリ)で問題になる構造です。[1]
| 順番 | 利用者の操作 | 裏側で起きること |
|---|---|---|
| 1 | 正規サイトにログインする | ブラウザーが認証Cookieを保存する |
| 2 | 別サイトの案内ページを開く | そのページが正規サイトへの要求を誘発する |
| 3 | 案内ページを見ている | 条件を満たせばCookie付きの変更要求が届き、処理される |
| 4 | 元の画面を再読み込みする | 保存された新しい設定が反映される |
この例は、元のタブのボタンや入力欄が遠隔操作される攻撃ではありません。ブラウザーから送られた要求によって、サーバー側に保存されたユーザーの表示設定が変更されます。開いたままの画面は以前の表示を続けることがあります。F5で再読み込みして初めて、並び順が変わったことに気付くわけです。
表示設定は仕組みを理解するための例です。同じ問題が登録情報の変更やデータ削除にあれば、影響は大きくなります。
認証Cookieが付くことが、今回の例の前提
リンクを開けば、いつでもこの攻撃が成立するわけではありません。
認証Cookieが送られなければ、この例の要求はログイン済みユーザーとして処理されません。また、Cookieが届いても、サーバー側のCSRF対策が働けば変更は拒否されます。
SameSiteを省略しただけで必ずCookieが送られるとも限りません。現在のブラウザーには、省略時にLaxとして扱うものがあります。[2]
したがって、ここでの説明は「標準設定の⑥が簡単に攻撃される」という意味ではなく、成立条件がそろったときに、認証だけでは何を区別できないかを示しています。
認証で確認できることと、できないこと
認証Cookieの検証で確認するのは、その要求をどのユーザーとして扱えるかです。認可では、そのユーザーに操作の権限があるかを確認します。
しかし、正規の設定画面で保存ボタンを押した要求も、別のサイトがブラウザーに送らせた要求も、同じユーザーの認証Cookieが付く場合があります。
| 確認 | 分かること |
|---|---|
| 認証 | 誰としてアクセスしているか |
| 認可 | そのユーザーが操作してよいか |
| CSRF対策 | 別サイトからの偽造要求を受け付けないための条件を満たしているか |
CSRFでは、攻撃者がCookieの値を盗み取る必要がない場合があります。保存済みのCookieを添えるのは、利用者のブラウザーだからです。
CookieをJavaScriptから読めなくするHttpOnlyや、認証チケットの暗号化は大切ですが、それだけで要求のきっかけまで確認できるわけではありません。
なお、CSRF対策も人間の意思そのものを証明する仕組みではありません。正規サイト上で悪意あるスクリプトが動くXSSなどは、別の対策が必要です。[1]
そのために、送信制限と要求の検証を使う

対策を理解するときは、ブラウザーが送る段階と、サーバーが受け付ける段階に分けると整理できます。
Cookieを送る段階:SameSite
ブラウザーは、Cookieの用途を理解して送信しているわけではありません。Cookieごとに設定された送信先、パス、Secure、有効期限、SameSiteなどを見て、要求に添えるか判断します。
認証Cookieも偽造防止Cookieも、この点は同じです。違うのは保存する内容、サーバーでの使い道、設定された属性です。
攻撃用ページへ正規サイトのCookieを渡す、という意味ではありません。攻撃用ページをきっかけに、ブラウザーが正規サイトへ要求を送り、その送信先に対応するCookieを条件に応じて添えます。
SameSiteは、クロスサイトの要求にCookieを付けるかどうかに関係する属性です。[2]
| 設定 | 基本的な動き |
|---|---|
| Strict | クロスサイトの要求には送らない |
| Lax | クロスサイトPOSTでは通常送らない。トップレベルのGETによる画面移動などでは送る |
| None | SameSiteによる制限を外す。Secureが必要で、ブラウザーの別の制限も受ける |
Noneは、それだけで脆弱という意味ではありません。外部サイトとの連携などで必要な場合もあり、その構成に合った要求の検証を組み合わせます。
また、SameSiteの「サイト」と、スキーム・ホスト・ポートで区別する「オリジン」は同じではありません。同じスキーム・ホストでポートだけが違うローカルアプリは、別オリジンでも同じサイトです。別オリジンなら必ずSameSiteで止まる、とは考えられません。
要求を受け付ける段階:偽造防止トークン
サーバー側では、認証Cookieだけで変更要求を受け付けず、正規の画面を通じて渡したトークンなども検証します。
フォームの非表示項目としてトークンを送り返す方式は、広く使われています。JavaScriptからHTTPヘッダーに付けて送る方式もあります。
ただし、偽造防止Cookieが必須とは限りません。サーバー側のセッションに保持した情報と照合する方式や、Cookieと要求内のトークンを組み合わせる方式などがあります。[1]
フォーム内トークンや専用Cookieが見当たらないことだけで、対策がないとは判断できません。 何を使って要求を検証しているかを見る必要があります。
反対に、トークンが画面にあるだけでも不十分です。受け取ったサーバーが検証し、不正な要求を拒否するところまで実装して初めて対策になります。
⑥では、どう対策したか

ここからは、⑥で採用したASP.NET Coreの実装を具体例として見ます。Webサイトすべてが同じ方式を使っているわけではありません。
⑥には、次の三つの情報が登場します。
| 情報 | 保存・送信される場所 | 役割 |
|---|---|---|
| 認証Cookie | ブラウザーのCookie | ログイン済みユーザーを確認する |
| 偽造防止Cookie内のトークン | ブラウザーのCookie | CSRF対策で検証する情報の一方 |
| フォーム内トークン | HTMLの非表示項目。送信時はフォームデータ | CSRF対策で検証するもう一方の情報 |
開発者ツールに並んでいたSample.Authと.AspNetCore.Antiforgery…は、同じ用途のCookieではありません。前者は認証用、後者は偽造防止用です。
Cookieごとに送信ルールがある
⑥では認証CookieのSameSiteを明示的にLaxに設定しました。偽造防止Cookieは、ASP.NET Coreの既定値ではStrictです。[3]
| 場面 | 認証Cookie:Lax | 偽造防止Cookie:Strict |
|---|---|---|
| 正規サイト内のフォーム送信 | 送る | 送る |
| 別サイトからの通常のリンクによるGETの画面移動 | 送る | 送らない |
| 別サイトのフォームからのクロスサイトPOST | 送らない | 送らない |
これは、送信先や有効期限など、他の条件も満たす場合の基本例です。
⑥に別サイトからPOSTする例では、通常は認証Cookieが付かないため、ログイン済みユーザーとしての変更処理へ進めません。それに加えて、サーバー側には偽造防止トークンの検証もあります。
SameSiteはCookieに対する属性です。フォーム内トークン自体にSameSiteが設定されるわけではありません。
二つのトークンは同じ文字列ではない
ここでいう二つとは、偽造防止Cookie内のトークンと、フォーム内トークンです。認証Cookieは含みません。
サーバーは、対応関係のある別々のトークンを用意します。フォーム内トークンを、そのままCookieにも保存するわけではありません。既存の有効な偽造防止Cookieを利用して、フォーム側のトークンを生成する場合もあります。
正規フォームを送信すると、Cookieとして届くトークンと、フォームデータとして届くトークンが、一つの要求に含まれます。
サーバーは、それぞれのトークンの保護された内容を読み取り、Cookie用・要求用という種類や、二つの対応関係、ユーザーとの対応などを検証します。[4]
説明上は、次のように分けられます。
- 偽造防止Cookie内のトークンが存在し、有効なものか確認する。
- フォーム内トークンが存在し、有効なものか確認する。
- 二つが正しい組み合わせで、現在のユーザーに対応するか確認する。
これは検証内容を整理したもので、独立した三つの認証方式ではありません。また、Cookieとフォームの文字列をそのまま比較して、一致すれば成功という処理でもありません。
正規フォームのトークンは、通常、別オリジンの攻撃用ページから自由に読み取れません。攻撃者が同じ項目名で適当な値を送っても、正しい組み合わせにはなりません。[1]
フォームへの生成と、サーバーでの検証
⑥のログアウトフォームは、次の形でした。
<form asp-controller="Auth" asp-action="Logout" method="post">
<button type="submit">ログアウト</button>
</form>MVCのForm Tag Helperが有効な構成では、このPOSTフォームに偽造防止トークンの非表示項目が生成されます。ブラウザーが受け取るHTMLには、概念的には次の項目が入ります。[4]
<input name="__RequestVerificationToken"
type="hidden"
value="サーバーが生成したトークン" />この値を開発者が固定文字列で書くのではありません。上は生成結果の形を示した例です。
受け取る側のアクションには、次の属性を付けていました。
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await _signInManager.SignOutAsync();
return RedirectToAction(nameof(Index));
}Authorizeは認証を要求し、ValidateAntiForgeryTokenは偽造防止トークンを検証します。不足や不正があれば、通常は400 Bad Requestとなり、アクション内の処理へ進みません。
前回のログインPOSTにも、偽造防止トークンの検証を付けています。意図しないアカウントにログインさせる「ログインCSRF」への対策にもなるためです。
ASP.NET Coreに標準機能があることと、すべてのMVCアクションが自動的に検証されることは別です。⑥では、生成する側と検証する側をそろえていました。
正常な送信を、開発者ツールで確認する
⑥で作ったアプリを使い、正常な要求にどの情報が含まれるか確認してみましょう。
- ログイン後、ApplicationのCookiesで、Sample.Authと.AspNetCore.Antiforgery…のSameSiteを確認する。
- Elementsでログアウトのformを探し、__RequestVerificationTokenという非表示項目を見る。
- NetworkでPreserve logを有効にし、ログアウトボタンを押す。
- ログアウトのPOSTを選び、PayloadのForm Dataにトークンがあるか確認する。
- HeadersのCookieなどで、送信されたCookieも確認する。
これにより、Cookieの保存、フォーム内への埋め込み、実際の送信をつなげて見ることができます。サーバー側の検証は、対応する属性と実装を確認します。送信画面が見えたことだけで、サーバーの検証まで証明できるわけではありません。
⑥で確認したCookie一覧に、もう一つの意味が加わります。認証Cookieはログイン状態を引き継ぎ、偽造防止用の情報は要求を受け付ける際の検証に使われていたのです。
GETで更新しないことも、対策の一部
SameSite=Laxでは、別サイトからのリンクによるGETの画面移動にCookieが付く場合があります。そのため、GETで設定変更や削除を行う実装なら、Laxだけで安全とは言えません。
状態を変更する処理はPOSTなどに分け、必要なCSRF対策を組み合わせます。POSTに変えただけでも、偽造要求を識別できるようになるわけではありません。[1]
CSRF対策を考える際には、「ログインしているか」だけでなく、「どの要求にCookieが付き、届いた要求を何で検証するか」を確認します。
次回
次は、URLを開いて目的の画面へ進む仕組みを取り上げます。
⑥でも登場したReturnUrlが、ログイン前後の画面移動をどうつなぐのか。URLによる画面の指定と、認証そのものを分けて整理していきます。