ExpressでCSRF対策をする

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、まだまだExpressに関わる記事を続けて投稿していますが、やはりというかいつも「マジカル」とまで評価されたLaravelとの比較を心のどこかでやっている自分がいたりします(笑)

そして、この間も「あ、そういえばあの機能がExpressにもほしいな〜」なんていう思ってしまいました。

その機能はというと・・・

CSRF(クロスサイトリクエストフォージェリ)対策

です。

※もし、まだCSRFを聞いたことがない人はストーリー仕立てで紹介していますので、ぜひ以下の記事をご覧ください。(時間が無い方は、とにかく「なりかわり対策」と考えておいてください)

【参考記事】CSRF(クロスサイト・リクエスト・フォージェリ)攻撃をできるだけ分かりやすく解説

そこで!

今回はExpressCSRF対策を施してよりセキュアなウェブサイトにしてみたいと思います。

ぜひ皆さんのお役に立てると嬉しいです😊✨

開発環境: 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送信)

ただし、そのまま送信しただけで処理がストップしないので今回はわざとトークンをGoogleDevToolで変更してから送信することにします。

<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送信です。(こちらもトークンをわざと改変しています)

はい、こちらも処理がストップしました。
成功です😊✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はExpressCSRF対策を施してみました。デフォルトでは何も対策をしていないExpressアプリがより格段にセキュアになることと思います。

ぜひ皆さんも試してみてくださいね。

ではでは〜!

このエントリーをはてなブックマークに追加       follow us in feedly