【初心者向け】Express.js(Node.js) で CSRF 対策の実装方法
Express.js では、CSRF対策を実装する方法についてのメモ書きです。formタグで行う時と、非同期処理で行う方法について記載しています。Djangoならインストールした段階でCSRFは使えると思いますが、Express(Node.js)では『どうするんだっけ?』となったときに役に立つと思います。必要最低限のモジュールと、その使い方を覚えておきましょう。
目次
検証する環境
- OS:Ubuntu 20.04
 - npm:v10.9.1
 - Express:v4.21.2
 - csurf:v1.10.0
 
Express.js での CSRF 対策
Express.js では、csurfモジュールを利用することで、簡単に CSRF 対策を実装できます。csurfモジュールは、リクエストごとに一意なトークンを生成し、そのトークンをフォームに埋め込むことで、CSRF 攻撃を防ぐことができます。
CSRF 対策に必要なモジュール
- csurf
 - cookie-parser、express-session、body-parser
 
インストール
Expressの構築用:テンプレートエンジンにejsを使っています。
npm install  express nodemon  ejs CSRF対策用
npm install  body-parser  cookie-parser  csurf  express-sessionサーバー側の構成
(1)実装例
server.js
基本的な実装
const app = require('./app');  // 外部ファイルapp.js読み込み
const http = require('http');
const server = http.createServer(app);
const PORT = 3000;
server.listen(PORT, ()=>{
    console.log('server running!!');
 });app.js
CSRF対策に的を絞った実装(重要な箇所のみ)
const express = require('express');
const app = express();
const bodyParser = require('body-parser');   // ←req.body.id などで値を受け取るため
// ↓csrf対策  ***************************************
const session = require('express-session');  
const csrf = require('csurf');
const cookieParser = require('cookie-parser');
//  ↑ ***********************************************
const indexRouter = require('./routes/posts') // テスト用に作成
app.set('view engine', 'ejs');   // 使用するテンプレートエンジンの例
app.use(express.json());
app.use(bodyParser.urlencoded({extended: false})); 
// ↓ミドルウェアの設定 ***********************
/**  sessionの設定  **/
app.use(session({
    secret : 'secret_Key_qa', // ←なんでも良い
    resave : false,     //   true:リクエスト中にセッションが変更されなかった場合でも、
                                    //セッションを強制的にセッション ストアに保存し直す。
    saveUninitialized : false, // 未初期化状態のセッションも保存するようなオプション。今回はfalse.
    cookie : {
   // ↓クライアント側でクッキー値の書きかえが出来ないようにする
        httpOnly : true,   
       // ↓セキュア Cookie には HTTPS が必要。ここではhttp通信なのでfalseに設定
        secure: false,    
        expires: 3000000  
    } 
}));
app.use(cookieParser());
app.use(csrf());   
app.use(function(req, res, next) {        
    res.locals.csrftoken = req.csrfToken();   
    next();
});
// ↑ *******************************************
app.use('/', indexRouter);  // テスト用に作成
module.exports = app;(2)コードの簡単な説明
必要なモジュールを呼び出す。
const session = require('express-session');  
const csrf = require('csurf'); 
const cookieParser = require('cookie-parser');次のミドルウェアの設定で、テンプレートエンジンで、value="<%= csrftoken %>"が使えるようになります。
app.use(cookieParser());
app.use(csrf());   
// ※自分で作るmiddleware なので、最後にnext()を忘れないようにする!!
app.use(function(req, res, next) {        
 // ※res.locals.csrfToken に CSRF トークンを格納する。
    res.locals.csrftoken = req.csrfToken();   
    next();
});クライアント側の構成
(1) HTMLのformタグを使う方法
index.ejs
<h2>Express CSRF対策</h2>
    <ul >
        <% if (posts.length) { %>
            <% posts.forEach(post => { %>
                <li> <a href="/<%= post.id %>/detail"><%= post.title %></a> </li>
            <% }); %>
        <% } else { %>
            <li>ありません</li>
        <% } %>
             
    </ul>
    <form action="/create" method="post">
        <input type="text" name="title" >
        <input type="text" name="body" >
        <input type="hidden" name="_csrf" value="<%= csrftoken %>">
        <button type="submit">送信</button>
    </form><form>タグ内で<input type="hidden" name="_csrf" value="<%= csrftoken %>">を挿入すればOKです。name属性は必ず"_csrf"とします。
(2)非同期処理で行う場合
JQueryではライブラリを読み込まないといけないので、JavaScriptだけで使えるfetch()関数を使うことにします。
index.ejs
<body> 
--- 省略 -----
     <input type="text" name="title" id="sendtext">
    <input type="text" name="body" id="sendbody">
    <button type="button" id="asyncbtn" data-token="<%= csrftoken %>">送信</button>
    <script>
        {
            'use strict';
            const btn = document.getElementById('asyncbtn');
            const text = document.getElementById('sendtext');
            const sendbody = document.getElementById('sendbody');
            const url = "/create_async";
            btn.addEventListener('click', ()=>{
                const title = text.value;
                const body = sendbody.value;
                const token = btn.dataset.token;
                const option = {
                    method : 'POST',
                    headers: {
                        "Content-Type": "application/json",
                        // ↓テンプレートエンジンからトークンにアクセスできるようになります。
                        "X-CSRF-Token": token // CSRFトークンをカスタムヘッダーに含める
                    },
                    body: JSON.stringify({
                        title: title,
                        body : body
                    })
                }
                // ここから非同期処理
                fetch(url,option)
                .then(res=>res.json()).then(data=>{
                   // 成功
                    console.log(data);
    // その後何らかの処理を行う
                    --- 省略 ----
                }).catch(err=>{
                  // 失敗
                    console.error(err);
                })
            });
        }
    </script>
</body>- <button type="button" id="asyncbtn" data-token="<%= csrftoken %>">非送信</button>
const btn = document.getElementById('asyncbtn');
const token = btn.dataset.token; - headers: {
"Content-Type": "application/json",
"X-CSRF-Token": token
}, 
ポイントとしては、(1)後でDOM操作しやすいようにbuttonタグ内にdataカスタム属性を持たせていることと、(2)CSRFトークンをカスタムヘッダーに含めていることです。
使い方はとても簡単でしたが、設定を忘れてしまいそうですね。以上です。

