JavaScript | Web API:パフォーマンス・セキュリティ - CSRF 対策

JavaScript JavaScript
スポンサーリンク

CSRF は「ユーザーになりすまして勝手に操作させる攻撃」

まずイメージからいきます。

CSRF(Cross-Site Request Forgery)は、
「ログイン中のユーザーになりすまして、別サイトから勝手に操作させる」 攻撃です。

ポイントはここです。

ユーザーは「攻撃サイト」を見ているつもり
でも、その裏で「本物のサイト」にリクエストが飛ぶ
ログイン中なので、クッキーが自動で送られてしまう
結果として「本人が操作したことになってしまう」

例えば、こんな状況を想像してください。

あなたは銀行サイト bank.example.com にログイン中
別タブで怪しいサイト evil.example.com を開く
そのページの中に、見えないフォームが仕込まれている
そのフォームは「あなたの口座から攻撃者の口座に送金する」リクエストを送る

ブラウザは「bank.example.com へのリクエストだな。クッキー付けて送ろう」と勝手に判断します。
結果として、「あなたが送金ボタンを押したこと」になってしまう。

これが CSRF の本質です。


なぜ「クッキー」が絡むと危険になるのか

ブラウザは「同じサイトへのリクエストにはクッキーを自動で付ける」

CSRF が成立する条件のひとつは、
「認証がクッキーだけで管理されている」 ことです。

ブラウザは、こう動きます。

bank.example.com にログインする
→ サーバーが「セッション ID」をクッキーに入れて返す
→ 以後、bank.example.com へのリクエストには、そのクッキーが自動で付く

ここまでは普通です。

問題は、「どこからの JavaScript/HTML が送ったリクエストか」を
ブラウザは気にしない、という点です。

bank.example.com のページから送ったフォームでも
evil.example.com のページから送ったフォームでも
「行き先が bank.example.com ならクッキーを付ける」

これが CSRF の入り口になります。


典型的な CSRF のイメージ例

見えないフォームで勝手に POST させる

攻撃サイト側の HTML は、例えばこんな感じです。

<form id="attack" action="https://bank.example.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker-account">
  <input type="hidden" name="amount" value="100000">
</form>

<script>
  document.getElementById("attack").submit();
</script>

ユーザーがこのページを開いた瞬間、
フォームが自動送信されます。

ユーザーは bank.example.com を見ていないのに、
bank.example.com/transfer に POST が飛びます。

ブラウザは「bank.example.com へのリクエストだね」と判断し、
ログイン中ならセッションのクッキーを付けます。

サーバー側から見ると、

「ログイン中のユーザーが /transfer に POST してきた」
「内容は to=attacker-account, amount=100000」

という、完全に正当なリクエストに見える わけです。

だから、サーバー側で「本当に本人の意思かどうか」を
追加で確認する仕組みが必要になります。


CSRF 対策の王道: CSRF トークン(ワンタイムの秘密)

「フォームごとに秘密の合言葉を仕込んで、サーバーで照合する」

一番有名で強力な対策が、
CSRF トークン を使う方法です。

流れをざっくり言うと、こうです。

ユーザーがフォームのページを開く
→ サーバーが「ランダムなトークン」を発行して、HTML に埋め込む
→ ユーザーがフォームを送信すると、そのトークンも一緒に送られる
→ サーバーは「そのトークンが自分が発行したものか」をチェックする

攻撃サイトからは、このトークンを知ることができません。
だから、「正しいトークンが付いていないリクエストは拒否する」 ことで、
CSRF を防げます。

超ざっくりしたイメージコード

サーバー側(イメージ・擬似コード):

// フォームを表示するとき
app.get("/transfer", (req, res) => {
  const csrfToken = generateRandomToken();
  req.session.csrfToken = csrfToken;

  res.send(`
    <form method="POST" action="/transfer">
      <input type="hidden" name="csrfToken" value="${csrfToken}">
      <!-- 他の入力項目 -->
    </form>
  `);
});

// フォーム送信を受け取るとき
app.post("/transfer", (req, res) => {
  const tokenFromForm = req.body.csrfToken;
  const tokenInSession = req.session.csrfToken;

  if (!tokenFromForm || tokenFromForm !== tokenInSession) {
    return res.status(403).send("CSRF 検出");
  }

  // ここまで来たら正当なフォーム送信とみなす
  // 送金処理などを行う
});
JavaScript

ポイントはここです。

トークンはサーバー側で生成する
トークンはサーバー側(セッションなど)に覚えておく
フォームには hidden で埋め込む
POST されたトークンとサーバー側のトークンを照合する

攻撃サイトは、
ユーザーのセッションに保存されたトークンを知ることができません。
だから、正しいトークンを付けたリクエストを作れない、という仕組みです。


SameSite クッキーで「そもそもクッキーを送らせない」方向の対策

「他サイトからのリクエストにはクッキーを付けないで」とブラウザに頼む

最近のブラウザでは、
クッキーに SameSite 属性を付けることで、
「他サイトからのリクエストにはクッキーを送らない」
という設定ができるようになっています。

例えば、セッション用クッキーをこう設定します。

Set-Cookie: sessionId=xxxx; Path=/; Secure; HttpOnly; SameSite=Lax

SameSite=Lax は、

通常のリンクや GET リクエストにはクッキーを付ける
他サイトからの POST など「危険なリクエスト」にはクッキーを付けない

という挙動になります(ざっくり)。

SameSite=Strict にすると、
他サイトからのあらゆるリクエストにクッキーを付けなくなりますが、
UX 的に厳しい場面もあるので、
多くのサイトでは Lax が使われます。

これを設定しておくと、

evil.example.com から bank.example.com/transfer に POST しても、
セッションのクッキーが付かない
→ サーバー側から見ると「未ログインの人のリクエスト」に見える
→ 送金処理などは実行されない

という形で、CSRF の成功率を大きく下げられます。

ただし、SameSite だけで完全に安心、とは言えません。
ブラウザ依存・古いブラウザ・特殊なケースなどもあるので、
「CSRF トークン+SameSite クッキー」の二重で守る のが理想です。


JavaScript 側で意識しておくべきこと

「クッキー任せの認証」に甘えない設計を意識する

フロントエンド側から見た CSRF 対策のポイントは、
「クッキーだけに頼らない」 という意識です。

例えば、API を叩くときに

クッキーで自動的に認証される前提
→ CSRF のリスクが高い

トークン(例: JWT)を Authorization ヘッダに自分で付ける
→ 他サイトから勝手に送られにくい

という違いがあります。

もちろん、トークン方式にも別の注意点がありますが、
少なくとも「クッキーが勝手に付く」問題は避けられます。

また、フロントエンドでフォームを作るときも、

サーバーが埋め込んだ CSRF トークンを hidden で一緒に送る
fetch で POST するときも、ヘッダやボディに CSRF トークンを含める

といった形で、
「サーバーと一緒に CSRF トークンを運ぶ」 ことを意識します。


初心者として「CSRF 対策」で絶対に掴んでほしいこと

CSRF は、XSS みたいに「目に見える派手な攻撃」ではなく、
静かに「ユーザーになりすまして操作する」タイプの攻撃です。

だからこそ、設計段階での意識がめちゃくちゃ大事です。

ここだけは押さえておいてください。

CSRF は「ログイン中のクッキーが、他サイトからのリクエストにも勝手に付く」ことで起きる
対策の王道は「CSRF トークン」=サーバーが発行した秘密の合言葉をフォームやリクエストに必ず付ける
SameSite クッキーで「他サイトからのリクエストにはクッキーを送らない」設定をしておく
フロントエンドでは、サーバーから渡された CSRF トークンをちゃんと一緒に送る設計をする

あなたがコードを書くときに、
「このリクエスト、他サイトから勝手に送られたら困らない?」
と一度でも自問できるようになったら、
それはもう CSRF 対策のスタートラインに立てています。

そこから先は、
サーバーフレームワーク(Django, Rails, Laravel, Spring など)が用意している
CSRF 対策機能にちゃんと乗っかる、という実務の世界です。

「なんとなく怖い」から
「こういう仕組みで防ぐ」と言葉にできるようになった時点で、
セキュリティエンジニアとしての感覚が一段上がっています。

タイトルとURLをコピーしました