こんにちは!日々の開発やインフラの管理、本当にお疲れ様です。
セキュリティの世界へようこそ!新人のIT担当者さんや、「セキュリティってなんだか難しそう……」と感じている開発者の方に向けて、今日から使える実践的な知識を分かりやすくお伝えしていきますね。
今回のテーマは「CSRF(クロスサイトリクエストフォージェリ)」です。名前からして何やら難しそうですが、要は「ユーザーが意図しないうちに、勝手に裏で悪事を働かせられてしまう罠」のことです。
セキュリティの専門家として、この攻撃がどうやって仕掛けられ、どうすれば鉄壁の守りを作れるのか、身近な例えを交えながら一歩ずつ紐解いていきましょう!
—
1. 身近な例えで理解するCSRF(クロスサイトリクエストフォージェリ)
いきなりサーバーやプログラムの話をする前に、ちょっと身の回りの防犯を想像してみてください。
あなたは家に鍵をかけ、合鍵も信頼できる家族にしか渡していません。ブラウザに保存されている「ログイン状態(クッキー)」というのは、いわば「家に入り込むための合鍵を持っている状態」です。
ここで、こんなシチュエーションを想像してください。
1. あなたが、とあるショッピングサイトにログインしたまま、別の怪しいWebサイトを見に行きました。
2. その怪しいサイトの裏側には、見えない仕掛け(隠しボタンのようなもの)が隠されています。
3. その隠しボタンは、あなたがログインしているショッピングサイトに対して、「この人の全財産で怪しい壺を買う」という注文リクエストを勝手に送る命令が仕込まれていました。
4. あなたが怪しいサイトを開いた瞬間、ブラウザは「おっ、このショッピングサイトへの注文だね!合鍵(クッキー)もあるし、そのまま送っておくよ!」と、あなたの意思とは無関係に裏で注文を完了させてしまいました。
これがCSRFの恐ろしい仕組みです。
泥棒(攻撃者)はあなたの家(パスワードやアカウント情報)の鍵そのものを盗んだわけではありません。「あなたが今、鍵を開けてリラックスしている隙を突いて、あなた自身に勝手にお使いに行かせた」わけですね。
—
2. 攻撃者はどうやって罠を仕掛けるのか?(攻撃シーケンス)
実際のWebの世界では、この「お使い」はHTTPリクエストを使って行われます。
例えば、掲示板サイトなどでよくある「メールアドレス変更機能」を狙う攻撃を考えてみましょう。
ユーザーがログイン中の状態で、攻撃者が用意した悪意あるWebページ(http://evil.example.com/trap.html)にアクセスしたとします。そのページの中身は、次のようなHTMLコードになっています。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>プレゼント企画!今すぐクリック!</title>
</head>
<body>
<!-- ユーザーには見えない、またはクリックせざるを得ない罠のフォーム -->
<form id="csrfForm" action="https://bank.example.com/settings/change-email" method="POST">
<!-- 攻撃者のメールアドレスに勝手に書き換える隠しフィールド -->
<input type="hidden" name="new_email" value="attacker@evil.example.com">
</form>
<script>
// ページが読み込まれた瞬間に、自動でフォームを送信する
window.onload = function() {
document.getElementById('csrfForm').submit();
};
</script>
</body>
</html>
このコードの恐ろしいところは、被害者が「プレゼント企画だ!」と思ってページを開いた瞬間、JavaScriptによって自動的に bank.example.com(銀行やサービスのサイト)へ POST リクエストが飛んでしまう点です。
ブラウザは、「おっ、bank.example.com 宛てのリクエストだな?それなら、このユーザーがさっきから持っているセッションクッキーも一緒に添えておこう!」と親切心(お節介)からクッキーを自動添付して送信してしまいます。
サーバー側は「お、いつも使っているユーザーからの正しいリクエストだな。メールアドレスを書き換える処理を実行しよう!」と受け入れてしまい、乗っ取りが完了してしまいます。これがCSRFの全貌です。
—
3. 鉄壁の守り①:Anti-CSRFトークン(合言葉の仕組み)
「じゃあ、どうやって防げばいいの?」という話ですよね。
ここからが腕の見せ所です。一番確実で強力な対策が「Anti-CSRFトークン(ワンタイムトークン)」の実装です。
先ほどの例えに戻りましょう。
銀行の窓口で手続きをするとき、本人確認書類や、その場限りの「確認コード」を求められますよね。あれと同じです。
サーバー側があらかじめ「今からこのページでフォームを送信する人だけに渡す、他愛のないランダムな合言葉(トークン)」を発行し、フォームの中にこっそり仕込んでおきます。
実装のイメージ(PHPの例)
サーバー側でフォームを表示する際、セッションにランダムな文字列(トークン)を保存し、HTMLの隠しフィールド(input type="hidden")に埋め込みます。
<?php
// セッションを開始
session_start();
// まだトークンがなければ、推測されにくいランダムな文字列を生成してセッションに保存
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
?>
<!-- フォームの描画 -->
<form action="change-email.php" method="POST">
<!-- サーバーだけが知っている合言葉を隠しフィールドとして持たせる -->
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
<label>新しいメールアドレス:</label>
<input type="email" name="new_email" required>
<button type="submit">変更する</button>
</form>
そして、ユーザーが「変更する」ボタンを押してデータがサーバーに送られてきたとき、サーバー側では次のようなチェックを行います。
<?php
session_start();
// 送信されてきたPOSTデータと、サーバーのセッションにあるトークンを比較する
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// トークンが存在しない、または一致しない場合は攻撃とみなして弾く!
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
// エラー処理
die('セキュリティエラー: 不正なリクエストが検知されました。');
}
// --- ここから下に本来の安全な処理を書く ---
// 例: メールアドレスをデータベースで更新する処理など
echo 'メールアドレスが正常に変更されました!';
}
?>
なぜこれで防げるのか?
攻撃者の悪意あるサイト(evil.example.com)は、被害者のブラウザから勝手にリクエストを送ることはできても、被害者のセッションの中に隠されている「正しい合言葉(csrf_token)」を覗き見ることができません(同一生成元ポリシー:Same-Origin Policyというブラウザの基本ルールがあるためです)。
合言葉を知らない攻撃者は、適当な文字列を送るしかなく、サーバー側のチェックで「合言葉が違う!」と即座に見破られてブロックされるというわけです。
—
4. 鉄壁の守り②:SameSite属性(クッキーの身元確認)
もう一つの強力な武器が、Cookieに設定するSameSite属性です。
これは、ブラウザに対して「このクッキーは、どの範囲のアクセスまで一緒に送信していいよ」と指示する設定になります。
クッキーを発行する際、ヘッダーに次のように指定します。
Set-Cookie: session_id=abc123xyz; Secure; HttpOnly; SameSite=Lax
SameSiteには主に3つの設定値があります。
SameSite=Strict- 一番厳しい設定です。完全に「同じサイト内」からの移動でしかクッキーが送信されません。外部のサイトからのリンクや、別ドメインからのPOSTリクエストではクッキーが一切送られなくなります。セキュリティは最強ですが、外部サイトからリンクを踏んで自社サイトに来たときなどに「あれ、ログアウト状態になってる?」といった使い勝手の悪さ(UXの低下)が生じることがあります。
SameSite=Lax- 現代のウェブ標準におけるおすすめ設定です。通常のエントリー(ユーザーがサイトのリンクをクリックして移動するなど)ではクッキーを送りますが、CSRFの温床になりやすい「外部サイトからの勝手なPOSTリクエスト(フォーム送信やAjax通信など)」ではクッキーの送信をブロックしてくれます。セキュリティと利便性のバランスが非常に良い設定です。
SameSite=None- 外部サイトと連携する仕組み(APIやウィジェットなど)でどうしてもクッキーを共有したい場合に使います。ただし、この設定を使う場合は必ず通信が暗号化されていること(
Secure属性の併用)が必須条件となります。
新人エンジニアの皆さんがフレームワークやサーバーの設定(Nginx, Apacheなど、あるいは各言語のフレームワークのセッション設定)を行う際は、基本的にはセッションクッキーに SameSite=Lax(または厳格にやるなら Strict)がデフォルトで効いているかを必ず確認するようにしましょう!
—
まとめ:一歩ずつ、確実に安全なコードを書こう
いかがでしたでしょうか?
CSRFという一見難しそうな攻撃も、
1. 「ログイン中のブラウザの自動的なお節介(クッキー送信)」を悪用される仕組みであり、
2. 「Anti-CSRFトークン(合言葉)」でリクエストの正当性を証明し、
3. 「SameSite属性」でクッキーの持ち出し範囲を制限する
という2つの強力な盾を組み合わせることで、綺麗に、そして確実に防ぐことができます。
セキュリティ対策は、一度にすべてを完璧にやろうとすると息切れしてしまいます。ですが、今日学んだ「フォームには必ずワンタイムトークンを入れる」「クッキーにはSameSite属性を意識する」という基本の「き」を頭の片隅に置いておくだけで、あなたが作るWebアプリケーションはぐっと安全になります。
一歩ずつ、頼れるセキュリティ・エンジニアへの階段を登っていきましょう!次回の解説もお楽しみに!
コメント