
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、まだまだExpress
に関わる記事を続けて投稿していますが、やはりというかいつも「マジカル」とまで評価されたLaravel
との比較を心のどこかでやっている自分がいたりします(笑)
そして、この間も「あ、そういえばあの機能がExpress
にもほしいな〜」なんていう思ってしまいました。
その機能はというと・・・
CSRF(クロスサイトリクエストフォージェリ)対策
です。
※もし、まだCSRF
を聞いたことがない人はストーリー仕立てで紹介していますので、ぜひ以下の記事をご覧ください。(時間が無い方は、とにかく「なりかわり対策」と考えておいてください)
【参考記事】CSRF(クロスサイト・リクエスト・フォージェリ)攻撃をできるだけ分かりやすく解説
そこで!
今回はExpress
にCSRF
対策を施してよりセキュアなウェブサイトにしてみたいと思います。
ぜひ皆さんのお役に立てると嬉しいです
開発環境: Node 8、Express 4.1
目次 [非表示]
パッケージをインストールする
今回はCSRF
対策にセッションを使いますので、以下のパッケージをインストールしておいてください。
npm i --save express-session
パッケージを読み込む
次に、インストールしたパッケージとcrypto
(インストールは不要)をapp.js
で読み込んで使えるようにします。
const session = require('express-session');
const crypto = require('crypto');
ミドルウェアを設定する
同じくセッションデータが使えるようにするため、また、送信されてくるPOST
データを取得できるようにするために以下のミドルウェアを追加します。
// セッションの設定
app.use(session({
secret: 'YOUR-SECRET-STRING',
resave: true,
saveUninitialized: true
}));
// POSTデータの取得
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
CSRF対策のミドルウェアをつくる
では、本題のCSRF
攻撃を防ぐミドルウェアです。
app.use((req, res, next) => {
const method = req.method;
if(method === 'GET') {
const csrfToken = crypto.randomBytes(20).toString('hex');
req.session.csrfToken = csrfToken;
res.locals = {
csrfToken: csrfToken,
csrfField: '<input type="hidden" name="_token" value="'+ csrfToken +'">'
};
} else if(['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
if(req.body._token !== req.session.csrfToken) {
return res.status(419).send('Page Expired');
}
}
next();
});
この中でやっているのは、まずアクセスされたメソッドがGET
の時にCSRF
トークン(ワンタイムパスワード)を作ってセッションに格納、さらにres.locals
にも格納して、どのビューからでもCSRF
の情報が取得できるようにします。
(つまり、データ送信後にビューで設定したトークンとセッション内のトークンが一致すれば正しい送信として処理されるわけです)
そして、送信メソッドが以下のうちのいづれかの場合に、トークンをチェックし、間違っていたら処理をストップします。
- POST
- PUT
- PATCH(部分的なPUT)
- DELETE
使い方
では、ここからは今回開発したCSRF
対策ミドルウェアを実際につかう方法をご紹介します。
通常の送信をする場合
つまり、HTTP
リクエストをする場合のCSRF
対策です。
<html>
<head></head>
<body>
<div>
<form method="post" action="/csrf">
{{{ csrfField }}}
<button type="submit">送信する</button>
</form>
</div>
</body>
</html>
この中では、先ほどres.locals
内に格納したcsrfField
(hiddenタグ)をフォーム内にセットしています。
Ajaxで送信する場合
次にaxios
などでAjax通信でデータ送信する場合のCSRF
対策です。
<html>
<head>
<meta name="csrf-token" content="{{ csrfToken }}">
</head>
<body>
<div>
<button type="button" onclick="onSubmit()">Ajax送信</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>
function onSubmit() {
// MetaからCSRFトークンを取得
const csfrToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const url = '/csrf';
const params = {
_token: csfrToken
};
axios.post('/csrf', params)
.then(response => {
// 成功した場合
console.log(response.data)
})
.catch(error => {
// エラーの場合
console.log(error.response.data);
});
}
</script>
</body>
</html>
この中で重要なのは、トークンが<meta>
タグの中に格納されていることです。こうすることで、もし複数の場所でトークンが必要になってもquerySelector()
などを使って取得がしやすいからです。
テストしてみる
では、以下のようなビューをつくってそこから送信をしてみます。(HTTP
送信&Ajax
送信)
ただし、そのまま送信しただけで処理がストップしないので今回はわざとトークンをGoogle
のDevTool
で変更してから送信することにします。
<html>
<head>
<meta name="csrf-token" content="{{ csrfToken }}">
</head>
<body>
<div id="app">
<form method="post" action="/csrf">
{{{ csrfField }}}
<button type="submit">送信する</button>
</form>
<button type="button" onclick="onSubmit()">Ajax送信</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.2/axios.min.js"></script>
<script>
function onSubmit() {
const csfrToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const url = '/csrf';
const params = {
_token: csfrToken
};
axios.post('/csrf', params)
.then(response => {
console.log(response.data)
})
.catch(error => {
console.log(error.response.data);
});
}
</script>
</body>
</html>
ブラウザで表示するとこうなります。
では、まずはHTTP
送信です。(先ほど書いたとおり、トークンを改変しています)
はい。419が返ってきて処理がストップしました。
ブラウザでは以下の表示になります。
続いてAjax送信です。(こちらもトークンをわざと改変しています)
はい、こちらも処理がストップしました。
成功です
おわりに
ということで、今回はExpress
にCSRF
対策を施してみました。デフォルトでは何も対策をしていないExpress
アプリがより格段にセキュアになることと思います。
ぜひ皆さんも試してみてくださいね。
ではでは〜!