CSRF — ログイン中にリンクを踏むだけで、「削除」が押される

CSRF(クロスサイトリクエストフォージェリ)は、ログイン中の人が別サイトを開いただけで、あなたのアプリに「送信」「削除」が実行される穴。ブラウザは出どころを見ずに Cookie を付けるので、サーバーが確かめるしかない。直すのは SameSite=Lax と、書き換えを POST で受けること。AIが SameSite=None や GET 削除を書く瞬間と、test.html での確かめ方まで。

動画で見る

結論から

CSRF(クロスサイトリクエストフォージェリ)は、あなたのアプリにログインしている人が、別のサイトのリンクを踏んだだけで、あなたのアプリに「送信」「削除」「送金」が実行されてしまう穴です。本人は何も押していない。でもブラウザは、そのリクエストにログイン状態(Cookie)を付けて送ってしまう。

この記事で伝えたいのは、この一点だけです。ブラウザは、どのサイトから出たリクエストにも Cookie を付ける。だから、サーバーは「本当にうちの画面から押されたか」を確かめないといけない。 確かめ方は2つで、Cookie に SameSite=Lax を付けることと、書き換える操作を GET で受けないこと。どちらも AI に一言で頼めます。

  • 何が起きるか:攻撃者が自分のサイトに「あなたのアプリの削除 URL へ自動送信する仕掛け」を置く。ログイン中の人がそのページを開くと、本人の Cookie 付きで削除が実行される
  • なぜ:ブラウザは「宛先のサイトの Cookie」を、リクエストがどこから出たかに関係なく付ける。サーバーから見ると、本人の操作と区別がつかない
  • 直す:Cookie に SameSite=Lax(他サイト発のリクエストには付けない)。書き換える操作は POST のみで受け、Origin ヘッダを確認する。フレームワークの CSRF トークン機能を使う
  • 確かめる:パソコンで HTML ファイルを1つ作り、自分のアプリの削除 URL へ自動送信させる。ログイン中に開いて、削除が通ったら ✗

動画版(約2分半)は本文の上に埋め込んであります。

① 何が起きるのか:リンク1つで「削除」が押される

例として、AIで作った予約アプリに「予約をキャンセル」ボタンがあるとします。ボタンを押すと、ブラウザはこう送ります。

POST /api/reservations/12/cancel(Cookie: ログイン状態)

サーバーは Cookie を見て「佐藤さん本人だ」と確認し、キャンセルします。ここまでは正常です。

では、攻撃者が自分のブログにこんな仕掛けを置いたら。

<form action="https://あなたのアプリ/api/reservations/12/cancel" method="POST"><script>document.forms[0].submit()</script>

佐藤さんがそのブログを開いた瞬間、佐藤さんのブラウザが、佐藤さんの Cookie を付けて、あなたのアプリへ「キャンセル」を送ります。サーバーから見ると、佐藤さんが押したのと全く同じリクエストです。佐藤さんは何も押していないのに、予約が消えます。

2005年、日本の SNS mixi で「ぼくはまちちゃん!」という日記が大量に投稿された事件がありました。あるページを開くだけで、開いた本人の名前で日記が投稿される仕掛けでした。誰もボタンを押していない。これが CSRF です。

CSRF:ブラウザは出どころを見ずにCookieを付ける。サーバーが確かめる

② なぜ起きるのか:ブラウザは「どこから出たか」を見ない

原因はブラウザの基本動作にあります。ブラウザは、リクエストの宛先が あなたのアプリ なら、あなたのアプリ の Cookie を付けます。そのリクエストが、あなたのアプリの画面から出たのか、攻撃者のブログから出たのかは、見ません。

だから、サーバー側で「これは本当にうちの画面から出たか」を確かめる必要があります。手段は3つあります。

① Cookie に `SameSite=Lax` を付ける

「他のサイトから出たリクエストには、この Cookie を付けない」とブラウザに指示する属性です。Chrome は2020年(バージョン80)から、指定が無ければ Lax を初期値にしました。多くのアプリは、これだけで守られています。

② 書き換える操作は `POST` で受け、`GET` で受けない

SameSite=Lax は、リンクを踏む(GET)には Cookie を付けます。もし削除が GET /api/delete?id=12 で動くなら、リンク1つで削除されます。書き換えは必ず POST(または DELETE)にします。

③ CSRF トークン/`Origin` ヘッダの確認

フォームごとに「うちの画面が発行した合言葉」を付け、サーバーで照合する。あるいは Origin ヘッダが自分のドメインか確認する。Next.js の Server Actions や Rails などは、これを最初から組み込んでいます。

AIはどう書くか。 危ないのは3つの場面です。1つ目、「ボタンじゃなくてリンクで削除できるようにして」と頼み、AI が GET で削除を作るとき。2つ目、「別ドメインのフロントから Cookie が送られない」というエラーを直すために、AI が SameSite=None を書くとき(→ CORS の記事)。3つ目、Express などで自前の API を組み、Origin 確認もトークンも無いとき。どれも「動く」ので、気づきません。

③ どう直すのか:AIに「SameSite と POST」を頼む

以下をそのまま AI ツールに貼ってください。

「このプロジェクトのセッション Cookie の属性を確認し、SameSite=Lax(または Strict)、HttpOnly、Secure を付けてください。SameSite=None になっている場合は、その理由を説明してください。次に、データを作成・更新・削除する API エンドポイントをすべて列挙し、GET で受けているものは POST / PUT / DELETE に変更してください。最後に、それらのエンドポイントで Origin ヘッダ(または CSRF トークン)を確認していることを確認し、無ければ追加してください。」

ポイントは、「SameSite=None の理由を説明して」と言うこと。AI が CORS エラーを消すために無言で None にしていることがあり、それが CSRF の穴を開けます。

④ 直ったかを確かめる:HTML ファイルを1つ

  • パソコンで `test.html` を作り、中身をこうする:<form action="https://自分のアプリ/api/(何か削除・更新する URL)" method="POST"><button>送信</button></form>
  • 自分のアプリにログインしたまま、その test.html をブラウザで開き、ボタンを押す
  • 削除・更新が実行されたら ✗。「403」「ログインしていません」で止まれば ◯
  • ブラウザの開発者ツール → Application → Cookies で、セッション Cookie の SameSite 列が Lax か Strict になっているか見る。空欄か None なら要注意
  • 分からなければ、AIに「別オリジンからのフォーム送信でセッション Cookie が付かないこと、GET で状態が変わるエンドポイントが無いことを確認するテストを書いて」

自分で直せないとき:エンジニアに送る一文

「別サイトに置いたフォームから、ログイン中のユーザーの操作(削除・更新)が通ってしまいます。セッション Cookie の SameSite 属性と、状態を変えるエンドポイントの Origin 確認(または CSRF トークン)を確認・追加してください。」

よくある質問

XSS と何が違うのですか。

XSS は「攻撃者のスクリプトが、あなたのアプリの画面の中で動く」。CSRF は「攻撃者のサイトから、あなたのアプリへリクエストが飛ぶ」。CSRF の方が単純で、守り方も単純です(SameSite と POST)。ただし XSS があると CSRF 対策は無意味になります(あなたの画面の中から送れるので)。両方塞ぎます。

Supabase / Firebase の認証を使っています。関係ありますか。

これらの多くは Cookie ではなく「トークンをヘッダに付けて送る」方式で、その場合、他サイトのフォームからはトークンが付かないため CSRF は起きにくいです。ただし、Next.js の Server Components などでCookie にセッションを入れる構成もあり、その場合は関係あります。AIに「セッションは Cookie ですか、ヘッダのトークンですか」と聞いてください。

`SameSite=Strict` にすれば完璧ですか。

Strict は、他サイトからのリンクで自分のアプリを開いたときにもログアウト状態に見える(Cookie が付かない)ので、使い勝手が落ちます。多くのアプリは Lax で十分です。

この穴が塞がれば、削除機能は安全ですか。

「他サイトから勝手に押される」ことは防げます。「他人の予約を自分が削除できる」(→ URL の数字を1つ変える)は別の穴で、そちらは本人確認(認可)の話です。両方確認してください。