【初心者向け】Express.js(Node.js) で CSRF 対策の実装方法

Express.js では、CSRF対策を実装する方法についてのメモ書きです。formタグで行う時と、非同期処理で行う方法について記載しています。Djangoならインストールした段階でCSRFは使えると思いますが、Express(Node.js)では『どうするんだっけ?』となったときに役に立つと思います。必要最低限のモジュールと、その使い方を覚えておきましょう。

検証する環境

  1. OS:Ubuntu 20.04
  2. npm:v10.9.1
  3. Express:v4.21.2
  4. csurf:v1.10.0

Express.js での CSRF 対策

Express.js では、csurfモジュールを利用することで、簡単に CSRF 対策を実装できます。csurfモジュールは、リクエストごとに一意なトークンを生成し、そのトークンをフォームに埋め込むことで、CSRF 攻撃を防ぐことができます。

CSRF 対策に必要なモジュール

  1. csurf
  2. 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>
  1. <button type="button" id="asyncbtn" data-token="<%= csrftoken %>">非送信</button>
    const btn = document.getElementById('asyncbtn');
    const token = btn.dataset.token;
  2. headers: {
      "Content-Type": "application/json",
      "X-CSRF-Token": token
    },

ポイントとしては、(1)後でDOM操作しやすいようにbuttonタグ内にdataカスタム属性を持たせていることと、(2)CSRFトークンをカスタムヘッダーに含めていることです。

使い方はとても簡単でしたが、設定を忘れてしまいそうですね。以上です。

Follow me!