導入
現在ウェブアプリケーションの勉強をしています。
その過程で、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を付与していないフォームです。
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')
extends layout
block content
a(href="/login") Login(csrfToken有り)
br
a(href="/login_evil") Login evil(csrfToken無し)
br
if (certification)
p 認証成功です
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)
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ライブラリを使います。
// ここから追記==================================
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です。
「login(csrfToken有り)」をクリックすると、csrfTokenが埋め込まれたwebフォームページに遷移します。
以下の画像のように、確かにcsrfTokenがhidden属性で埋め込まれているのがわかります。
今回は認証機能を実装していませんので、usernameとpasswordにどんな文字列をいれてもらっても次に進みます。
適当な文字列をいれて、submitボタンを押してください。
上図のように、「認証成功です」と出ていればOKです。
どのようなデータを送信しているのかをwiresharkでチェックしてみましょう。
上図を見ると、確かにusername、passwordと_csrfの値を送信しているのが分かります。
次に、「login(csrfToken無し)」のページへ遷移してください。
以下の画像のように、こちらのwebフォームにはcsrfTokenが埋め込まれていません。
こちらでも同様に、usernameとpasswordに適当な文字列をいれて、submitボタンを押してください。
すると、上図のようにinvalid csrf tokenというエラー画面が表示されます。
送信したデータの中にcsrfTokenがないので、403(forbidden)が帰ってきています。
では、wiredsharkで送信したデータをチェックしてみましょう。
上図を見ると、確かに_csrfは送信していませんね。
Part1まとめ
csrfTokenは全てのページに埋め込むものではありません。
データを送信するページにのみ埋め込むようにします。
CSRF対策がされていないと、ログイン中に悪意のあるサイトへ遷移しただけで、勝手にアカウント情報が書き換えられてしまうので、ウェブサプリを作成する時は、注意しましょう。