JavaScript
Node.js
Express

ウェブアプリでCSRF対策してみる(Part1)

導入

現在ウェブアプリケーションの勉強をしています。
その過程で、CSRFという攻撃と、その対策について知りましたのでまとめました。

CSRFとは

CSRFとはクロスサイト・リクエスト・フォージェリの略だそうです。
簡単に説明します。

AさんはSNSサイトにログイン中です。
SNSサイトを眺めている時、面白そうなリンクを見つけたので、リンクをクリックしました。
(実はこのリンクは悪意のあるスクリプトが埋め込まれたサイトでした)
そして、翌日Aさんのアカウント情報が勝手に誰かに変更されており、AさんはSNSサイトにログインすることが出来なくなってしまいましたorz。

ここで問題となってくるのは以下の点です。

SNSサイトが適切なCSRF対策を行なっていなかったこと

本来であれば、Aさんのアカウント情報の変更はAさんのみ可能なはずです。
しかし、CSRF対策を行なっていないと、Aさんがログイン状態で悪意のあるサイトへのリンクをクリックをすると、Aさんに許可された全ての操作を第三者が行うことが可能になります。

記事の概略

この記事は二部構成になっています。
Part1では基本的なCSRF対策として、formにcsrfTokenを埋め込む作業を行います。
これによって、csrfTokenがない状態でデータをPOSTすることは出来なくなります。
Part2では、より実践的な内容として、CSRF対策がされていない状態で第三者がアカウント情報を書き換える攻撃も行います。そして、CSRF対策を講じることで、実際に第三者がアカウント情報を書き換えることができないことを確認します。

環境構築

$ npm install express-generator -g

$ express csrfTest
$ cd csrfTest
$ npm install
$ npm install csurf --save
$ npm install passport --save
$ npm install passport-local --save
$ npm install express-session --save

//webサーバ起動
$ node ./bin/www

一旦、ブラウザでlocalhost:3000にアクセスしてください。
welcome to expressと表示されていればOKです。

なお、今後webサーバの再起動といった場合は、以下の処理の事をさすこととします。

Ctrl + C
$ node ./bin/www

View関係のファイルを追加・編集

今回は「views/layout.jade, views/index.jade, views/login.jade, views/login_evil.jade」の四つのファイルを編集します。
「views/login.jade, views/login_evil.jade」はデフォルトでは存在しないので、新規に作成してください。
login.jadeはcsrfTokenを付与したフォームで、login_evil.jadeはcsrfTokenを付与していないフォームです。

views/layout.jade
doctype html
html
  head
    title= title
    meta(name='viewport', content='width=device-width, initial-scale=1.0')
    link(href='http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css', rel='stylesheet', media='screen')
    link(rel='stylesheet', href='/stylesheets/style.css')
  body
    block content

  script(src='http://code.jquery.com/jquery.js')
  script(src='http://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/js/bootstrap.min.js')
views/index.jade
extends layout

block content
  a(href="/login") Login(csrfToken有り)
  br

  a(href="/login_evil") Login evil(csrfToken無し)
  br

  if (certification)
    p 認証成功です


views/login.jade
extends layout

block content
  .container
    h1 Login Page
    p.lead デモ用のログインページです(csrfTokenあり)

    br
    form(role='form', action="/login",method="post", style='max-width: 300px;')
      .form-group
          input.form-control(type='text', name="username", placeholder='Enter Username')
      .form-group
        input.form-control(type='password', name="password", placeholder='Password')
      button.btn.btn-default(type='submit') Submit
       
      a(href='/')
        button.btn.btn-primary(type="button") Cancel
      input(type='hidden' name='_csrf' value=csrfToken)  
views/login_evil.jade
extends layout

block content
  .container
    h1 Login Page
    p.lead デモ用のログインページです(csrfTokenなし)

    br
    form(role='form', action="/login",method="post", style='max-width: 300px;')
      .form-group
          input.form-control(type='text', name="username", placeholder='Enter Username')
      .form-group
        input.form-control(type='password', name="password", placeholder='Password')
      button.btn.btn-default(type='submit') Submit
       
      a(href='/')
        button.btn.btn-primary(type="button") Cancel

CSRF対策を実装

実際にwebフォームにcsrfトークンを埋め込んでみましょう。
今回は、csurfというnpmライブラリを使います。

routes/index.js
// ここから追記==================================
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
// ここまで追記==================================

var express = require('express');
var router = express.Router();

var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })


router.get('/', function(req, res, next) {
  res.render('index', { title: 'Express' });
});

// 以下追記=========================================================
router.get('/login', csrfProtection, function(req, res) {
  // csrfToken付きでページを返す
  res.render('login', { csrfToken: req.csrfToken() })
});

router.get('/login_evil', csrfProtection, function(req, res) {
  // csrfToken無しでページを返す
  res.render('login_evil')
});

router.post('/login', parseForm, csrfProtection, function(req, res) {
  res.render('index', { certification: 'ok' })
});
// ここまで========================================================

module.exports = router;

動作確認をしましょう。
まず、webサーバを再起動してください。
「localhost:3000」にブラウザでアクセスして以下の画像のように表示されていればOKです。

Express-2.png

「login(csrfToken有り)」をクリックすると、csrfTokenが埋め込まれたwebフォームページに遷移します。
以下の画像のように、確かにcsrfTokenがhidden属性で埋め込まれているのがわかります。

localhost_3000_login.png

今回は認証機能を実装していませんので、usernameとpasswordにどんな文字列をいれてもらっても次に進みます。
適当な文字列をいれて、submitボタンを押してください。

localhost_3000_login-2.png

上図のように、「認証成功です」と出ていればOKです。

どのようなデータを送信しているのかをwiresharkでチェックしてみましょう。

Loopback__lo0__port_3000__からキャプチャ中.png

上図を見ると、確かにusername、passwordと_csrfの値を送信しているのが分かります。

次に、「login(csrfToken無し)」のページへ遷移してください。
以下の画像のように、こちらのwebフォームにはcsrfTokenが埋め込まれていません。

localhost_3000_login_evil.png

こちらでも同様に、usernameとpasswordに適当な文字列をいれて、submitボタンを押してください。

localhost_3000_login-3.png

すると、上図のようにinvalid csrf tokenというエラー画面が表示されます。
送信したデータの中にcsrfTokenがないので、403(forbidden)が帰ってきています。
では、wiredsharkで送信したデータをチェックしてみましょう。

Loopback__lo0__port_3000__からキャプチャ中-2.png

上図を見ると、確かに_csrfは送信していませんね。

Part1まとめ

csrfTokenは全てのページに埋め込むものではありません。
データを送信するページにのみ埋め込むようにします。
CSRF対策がされていないと、ログイン中に悪意のあるサイトへ遷移しただけで、勝手にアカウント情報が書き換えられてしまうので、ウェブサプリを作成する時は、注意しましょう。