こんにちは!ウェブ開発者の皆さん。💻
今日は、Node.jsとExpress.jsでウェブアプリケーションを構築する際に、本当に本当に重要でありながら、うっかり見落としがちなセキュリティ設定についてお話ししようと思います。それは、CSRF(Cross-Site Request Forgery、サイト間リクエスト偽造)攻撃を防ぐ方法についてです。
以下のようなコードをミドルウェア設定部分で見たことがありますか?
// ExpressのCSRF保護を有効にする
app.use(csrf());
// CSRFトークンをテンプレートで利用可能にする
app.use((req, res, next) => {
res.locals.csrftoken = req.csrfToken();
next();
});
この2つのコードの塊が、どのように私たちのサーバーを守ってくれるのか、その原理を簡単かつ詳細に掘り下げてみましょう!

🤔 ちょっと待って、CSRF攻撃って一体何ですか?
コードを見る前に、私たちが防ごうとしている敵、CSRFが何なのか簡単に見ていきましょう。
CSRFは、ユーザーが自分の意思とは無関係に、攻撃者が意図した行動をさせられる攻撃です。
例を挙げてみましょうか?
- ユーザー 👤 はmy-bank.comにログインした状態です。ブラウザにはmy-bank.comの認証情報(クッキー)が残っています。
- この時、ユーザーが悪意のあるメールや掲示板に含まれるリンク evil-site.com をクリックします。😈
- evil-site.com のページには、目に見えないコードが隠されています。例えば、my-bank.com/transfer?to=hacker&amount=1000 のようなリクエストを自動的に送信する画像タグやフォームが隠されているかもしれません。
- ユーザーのブラウザはこのリクエストを送信する際、my-bank.comの認証クッキーを自動的に一緒に送ります。🍪
- 銀行サーバー 🏦 の立場では?正常な認証クッキーが含まれたリクエストが来たので、「ああ、正常なユーザーが送金を要求しているんだな!」と勘違いし、攻撃者の口座にお金を送ってしまいます。
恐ろしいですよね?ユーザーはただリンクを一つクリックしただけなのに、自分も知らない間に重要な情報が変更されたり、金銭的被害を受ける可能性がある、非常に危険な攻撃です。
🔐 1段階: CSRF防御シールドを有効化! app.use(csrf());
では、最初のコードを見てみましょう。
// ExpressのCSRF保護を有効にする
app.use(csrf());
この一行のコードは、私たちのExpressサーバーに強力な防御シールドを設置するのと同じです。このコードが追加されると、サーバーは次のようなことを開始します。
- 秘密の鍵(CSRFトークン)の発行 🔑: サーバーは、各ユーザーセッションごとに固有で予測不可能な文字列である「CSRFトークン」を生成します。このトークンは、まるで使い捨てのパスワードのようです。
- リクエストの監視と検証 🧐: これからサーバーは、状態を変更するすべてのリクエスト(POST、PUT、DELETEなど)に対して、この秘密の鍵(CSRFトークン)が正しく含まれているかを検査します。
- リクエストにトークンがない、またはサーバーが発行したトークンと一致しない場合は? ➡️ ブロック! ❌ サーバーは該当リクエストを危険なものとみなし、エラーを発生させてリクエストを拒否します。
これで、攻撃者 😈 がevil-site.comからこっそりリクエストを送っても、この秘密の鍵(CSRFトークン)を知らないため、サーバーはリクエストをブロックします。これにより、CSRF攻撃のほとんどを防ぐことができます!
💌 2段階: 秘密の鍵をフロントエンドに安全に渡す
さて、サーバーは秘密の鍵を作成し、検証する準備ができました。しかし、一つ問題が残っています。
「ユーザーが正常に送るリクエストには、どうやってこの秘密の鍵を含めるのですか?」🤔
サーバーだけが秘密の鍵を知っていてもいけませんよね。ユーザー側(フロントエンド)でもこの鍵を受け取って、リクエストを送る際に一緒に送る必要があります。まさにこの役割を2番目のコードが担当します。
// CSRFトークンをテンプレートで利用可能にする
app.use((req, res, next) => {
res.locals.csrftoken = req.csrfToken();
next();
});
このコードは、すべてのリクエストに対して順次次の作業を実行します。
- req.csrfToken()の呼び出し: csrf()ミドルウェアがリクエスト(req)オブジェクトに作成してくれたcsrfToken()関数を呼び出します。この関数は、現在のセッションに対する秘密の鍵(CSRFトークン)を返します。
- res.localsに保存: res.localsは、該当リクエストを処理する間だけ有効なデータを格納する特別なポケットです。ここにcsrftokenという名前で、先ほど取得した秘密の鍵を保存します。
- テンプレートで利用可能: res.localsに保存された値は、EJS、Pug、Nunjucksのようなテンプレートエンジンで直接変数のように使用できます。つまり、HTMLをレンダリングする際に、この秘密の鍵の値を簡単に埋め込むことができるようになったのです!
✨ 最終段階: フロントエンドで秘密の鍵を使用する
これで全ての準備が整いました。フロントエンドテンプレート(例: form.ejsファイル)では、次のようにres.localsに保存しておいたトークンを使用できます。
<form action="/update-profile" method="POST">
<input type="hidden" name="_csrf" value="<%= csrftoken %>">
<div>
<label for="username">이름:</label>
<input type="text" id="username" name="username">
</div>
<button type="submit">프로필 업데이트</button>
</form>
このようにすると、ユーザーが「プロフィール更新」ボタンを押してフォームを送信する際、目に見えない _csrf フィールドに私たちの秘密の鍵(CSRFトークン)が格納されてサーバーに送信されます。サーバーはこのトークンを見て「ああ、私が発行した秘密の鍵を持つ正常なリクエストだな!」と判断し、リクエストを安全に処理します。✅
まとめ
今日見てきた2行のコードは、次のような完璧なCSRF防御システムを構築します。
- app.use(csrf());: CSRFトークンを生成し、入ってくるすべてのリクエストを監視し、トークンを検証する防御シールドを有効化します。
- app.use((req, res, next) => { … });: 防御に必要なCSRFトークンをフロントエンドテンプレートに安全に渡す役割をします。
- フロントエンド: 渡されたトークンをフォームの隠しフィールドに含め、サーバーに「私は正常なユーザーです」という証拠を一緒に送ります。
ウェブセキュリティはいくら強調しても足りません。特にユーザーの重要な情報を扱うサービスであれば、CSRF防御は選択ではなく必須です。このコードを通じて、皆さんの大切なサービスをより安全にしてください。
ハッピーコーディング!🚀
タグ: Nodejs, Express, CSRF, Security, WebDev, csurf, ウェブセキュリティ, Node.js
コメントを残す