【初心者向け】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トークンをカスタムヘッダーに含めていることです。
使い方はとても簡単でしたが、設定を忘れてしまいそうですね。以上です。