Node.jsのセキュリティ・チェックリスト

(訳注:2016/1/5、いただいた翻訳フィードバックを元に記事を修正いたしました。)

セキュリティ – 誰もが見て見ぬふりをする問題。セキュリティが重要だということは、誰もが認識していると思いますが、真剣にとらえている人は少数だと思います。我々、RisingStackは、皆さんに正しいセキュリティチェックを行っていただきたいと考え、チェックリストを用意しました。皆さんのアプリケーションが何千人というユーザやお客様に使用される前にセキュリティチェックを行ってください。

ここに挙げたリストのほとんどは概略的なもので、Node.jsに限らず、全ての言語やフレームワークに適用することができます。ただし、いくつのツールは、Node.js固有のものとなりますので、ご了承ください。Node.jsセキュリティに関するブログ記事も投稿してありますので、こちらも是非読んでみてください。

構成管理

HTTPヘッダのセキュリティ

Webサイトには、設定しておくべきセキュリティに関連するHTTPヘッダがあるので、ご紹介しておきます。

  • Strict Transport Securityは、サーバとの通信において、セキュアな(HTTP通信に対するSSL/TLS)接続を強制的に実行します。
  • X-Frame-Optionsを含めることで、クリックジャッキングを防止します。
  • X-XSS-Protectionを指定することで、最近のブラウザでは、クロスサイトスクリプティング(XSS)フィルタが有効になります。
  • X-Content-Type-Optionsは、ブラウザにおいて、宣言されたcontent-typeを無視するMIMEスニッフィングから防止します。
  • Content-Security-Policyは、クロスサイトスクリプティングやその他のクロスサイトインジェクションといったような、幅広い攻撃から守ります。

Node.jsでは、Helmetモジュールを使用することで、これらのヘッダを簡単に設定できます。

var express = require('express');  
var helmet = require('helmet');

var app = express();

app.use(helmet());  

Koaには、koa-helmetが使えます。

また、多くのアーキテクチャでは、実際のアプリケーションのコードを変更することなく、これらのヘッダをWebサーバのコンフィギュレーション(Apache, nginx)で設定することができます。Nginxでの設定は、以下のようになります。

# nginx.conf

add_header X-Frame-Options SAMEORIGIN;  
add_header X-Content-Type-Options nosniff;  
add_header X-XSS-Protection "1; mode=block";  
add_header Content-Security-Policy "default-src 'self'";  

全ての例は、nginx configurationファイルをご覧ください。

あなたのWebサイトに必要なヘッダが含まれているかどうかを確認したいようであれば、http://cyh.herokuapp.com/cyhオンラインチェッカーが利用できます。

クライアント側のセンシティブ情報

フロントエンドのアプリケーションをデプロイする場合、ソースコードに含まれるAPI secretやAPI認証が、絶対に露呈されないこと、そして誰にも解読できないようにすることに注意を払ってください。

自動でこれらを確認する良い方法はありませんが、クライアント側のセンシティブ情報が誤って露呈されてしまうリスクを軽減する、いくつかのオプションがあります。

  • プルリクエストを使う。
  • 定期的にコードの見直しを行う。

認証

総当たり攻撃からの保護

総当たり攻撃は、体系的に考えられる暗号をリストアップし、各暗号が問題のステートメントに適合しているかを試します。Webアプリケーションでは、ログインのエンドポイントが、まさにこの攻撃の対象と言えるでしょう。

これらの攻撃からアプリケーションを保護するには、rate-limitingといったような機能を実装する必要があります。Node.jsでは、ratelimiterパッケージが利用できます。

var email = req.body.email;  
var limit = new Limiter({ id: email, db: db });

limit.get(function(err, limit) {

});

もちろん、これをミドルウェアに含め、アプリケーションに入れ込むこともできます。ExpressとKoaの両フレームワークには、これに対応する素晴らしいミドルウェアがあります。Koaの場合の例を見てみましょう。

var ratelimit = require('koa-ratelimit');  
var redis = require('redis');  
var koa = require('koa');  
var app = koa();

var emailBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.body.email;
  }
});

var ipBasedRatelimit = ratelimit({  
  db: redis.createClient(),
  duration: 60000,
  max: 10,
  id: function (context) {
    return context.ip;
  }
});

app.post('/login', ipBasedRatelimit, emailBasedRatelimit, handleLogin);  

ここでは、与えられた時間内でユーザがログインを試すことができる回数を制限しました。これによって、総当たり攻撃されるリスクを軽減することができます。注意する点として、これらのコンフィギュレーションは、各アプリケーションに合わせて設定する必要がありますので、コピー&ペーストはしないでください。

このシナリオが、あなたのWebサイトでどのように振る舞うのか試したい場合は、hydraを利用することができます。

セッション管理

セキュアなクッキーを使うことの重要性を軽視することはできません。HTTPのようなステートレスなプロトコルを経由して状態を維持する必要があるような動的なWebアプリケーションでは特に重要となります。

クッキーのフラグ

以下は、各クッキーに対して設定することができる属性とその意味を説明しています。

  • セキュア – この属性は、リクエストがHTTPSを介して送信された場合にのみクッキーを送信するようブラウザに伝えます。
  • HttpOnly – この属性は、JavaScriptを経由してクッキーにアクセスすることを許可しないので、クロスサイトスクリプティングといった攻撃から保護する手助けをするのに使われます。

クッキーのスコープ

  • ドメイン – この属性は、URLがリクエストされるサーバのドメインと比較するのに使われます。ドメインがマッチする場合、もしくはそれがサブドメインである場合、次にパス属性が確認されます。
  • パス – ドメインに加え、クッキーが有効なURLパスが特定されます。ドメインとパスがマッチする場合、クッキーはリクエストに送られます。
  • 失効 – クッキーは、設定した日付が経過するまで失効しないので、この属性は、クッキーパーシステンスを設定するのに使われます。

Node.jsでは、cookieパッケージを使うことで、このクッキーを簡単に作成することができます。これには、マシン語が使われているので、恐らく、cookie-sessionといったような、ラッパーを使うことになるでしょう。

var cookieSession = require('cookie-session');  
var express = require('express');

var app = express();

app.use(cookieSession({  
  name: 'session',
  keys: [
    process.env.COOKIE_KEY1,
    process.env.COOKIE_KEY2
  ]
}));

app.use(function (req, res, next) {  
  var n = req.session.views || 0;
  req.session.views = n++;
  res.end(n + ' views');
});

app.listen(3000);  

(この例は、cookie-sessionモジュールのドキュメンテーションから抜粋しています。)

CSRF

CSRF(Cross-Site Request Forgery)とは、ユーザがアクセスしているWebアプリケーション上で、望んでいないアクションの実行を強制する攻撃です。攻撃者は、偽造されたリクエストに対してのレスポンスを見ることができないので、この攻撃は、データ盗用ではなく、特に状態を変更するリクエストを対象としています。

こういった攻撃を軽減するために、Node.jsではcsrfモジュールを利用することができます。これもまたマシン語が使われているので、異なるフレームワーク向けのラッパーがあります。例として、csurfモジュールがあります。これは、CSRFプロテクション向けのExpressミドルウェアです。

ルートのハンドラーレベルでは、以下のようにする必要があります。

var cookieParser = require('cookie-parser');  
var csrf = require('csurf');  
var bodyParser = require('body-parser');  
var express = require('express');

// setup route middlewares 
var csrfProtection = csrf({ cookie: true });  
var parseForm = bodyParser.urlencoded({ extended: false });

// create express app 
var app = express();

// we need this because "cookie" is true in csrfProtection 
app.use(cookieParser());

app.get('/form', csrfProtection, function(req, res) {  
  // pass the csrfToken to the view 
  res.render('send', { csrfToken: req.csrfToken() });
});

app.post('/process', parseForm, csrfProtection, function(req, res) {  
  res.send('data is being processed');
});

ビューレイヤでは、以下のようにCSRFトークンを使う必要があります。

<form action="/process" method="POST">  
  <input type="hidden" name="_csrf" value="{{csrfToken}}">

  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>  

(この例は、csurfモジュールのドキュメンテーションから抜粋しています。)

データバリデーション

XSS

ここでは、防御しなければならない、類似しつつも異なったタイプの攻撃を2つご紹介します。1つは、反射型クロスサイトスクリプティング、もう1つは蓄積型クロスサイトスクリプティングです。

Refrected(反射型) クロスサイトスクリプティングは、攻撃者が巧妙に作成したリンクを用いて、実行可能なJavaScriptのコードをHTMLレスポンスに注入することで発生します。

Stored(蓄積型) クロスサイトスクリプティングは、適切にフィルタされていないユーザの入力内容をアプリケーションが保存している際に発生します。なお、これが実行されるのは、Webアプリケーションの管理下にあるユーザのブラウザ上です。

このような攻撃を防ぐために、常にユーザの入力内容をフィルタして、サニタイズを行うようにしましょう。

SQLインジェクション

SQLインジェクションとは、ユーザからの入力を通じて、断片的または完全なSQLクエリを注入することです。これにより、機密情報の入手や破壊が可能になります。

以下の例を見てみましょう。

select title, author from books where id=$id  

この例では、$idはユーザが入力する内容です。では、ユーザが2 or 1=1と入力したら、どうなるでしょう? この場合、クエリは以下のようになります。

select title, author from books where id=2 or 1=1

このような攻撃を最も簡単に防ぐには、パラメータ化クエリやプリペアドステートメントを使うのが良いでしょう。

Node.jsのPostgreSQLを使用している場合は、恐らくnode-postgresモジュールを使っているはずです。パラメータ化クエリは、以下のようにするだけで作成できます。

var q = 'SELECT name FROM books WHERE id = $1';  
client.query(q, ['3'], function(err, result) {});

sqlmapは、オープンソースの侵入テストツールです。データベースサーバにおいて、SQLインジェクションの弱点とデータベースサーバ乗っ取りを検出・利用するのを自動化します。自身のアプリケーションにおいて、SQLインジェンクションの脆弱性をテストする際は、このツールを使ってみてください。

コマンドインジェクション

コマンドインジェクションは、遠隔WebサーバからOSコマンドを実行するために攻撃者が使うテクニックです。この手法で、攻撃者はシステムのパスワードまで取得できてしまう可能性があります。

例として、以下のURLがあるとしましょう。

https://example.com/downloads?file=user1.txt  

これは以下のように変更されることがあります。

https://example.com/downloads?file=%3Bcat%20/etc/passwd  

この例では%3Bがセミコロンになるので、複数のOSコマンドが実行される恐れがあります。

このような攻撃を防ぐために、常にユーザの入力内容をフィルタして、サニタイズを行うようにしましょう。


Node.jsについても触れておきましょう。

child_process.exec('ls', function (err, data) {  
    console.log(data);
});

child_process.execは内部で、/bin/shを実行するために呼び出しを行います。これはbashインタプリタであって、プログラムランチャではありません。

ユーザの入力内容がこのメソッドに渡されので、バッククォートや$()を混入される可能性があるので厄介です。そうなると、攻撃者に新しいコマンドを注入される恐れがあるのです。

この問題は、child_process.execFileを使うだけで解決できます。

セキュアトランスミッション

SSLバージョン、アルゴリズム、キーの長さ

HTTPはクリアテキストのプロトコルなので、HTTPSとして知られるSSL/TLSのトンネルによって保護されていなくてはなりません。近頃は、高度な暗号が一般的に使用されていますが、サーバの設定を間違えると、強制的に弱い暗号を使用している状態になったり、最悪の場合には何も暗号がかかっていない状態になったりする恐れがあります。

そのため、以下の内容をテストする必要があります。

  • 暗号、キー、再ネゴシエーションが適切に設定されていること
  • 証明書が有効であること

このテストは、nmapsslyzeというツールを使うと、簡単に行えます。

認証情報の確認

nmap --script ssl-cert,ssl-enum-ciphers -p 443,465,993,995 www.example.com  

sslyzeでSSL/TSLの脆弱性をテストする

./sslyze.py --regular example.com:443

HSTS

これについては設定管理のパートでも少し触れましたが、Strict Transport Securityヘッダは、サーバへの安全な(SSL/TLS経由のHTTP)接続を強制しています。以下のTwitterの例を見てみましょう。

strict-transport-security:max-age=631138519  

このmax-ageは、ブラウザが自動的に全てのHTTPリクエストをHTTPSに変換する秒数を定義します。

これは以下のようにすると、とても簡単にテストできます。

curl -s -D- https://twitter.com/ | grep -i Strict  

サービスの拒否

アカウントのロックアウト

アカウントのロックアウトは、総当たり推測攻撃を軽減するためのテクニックです。実際には、何回かログインに失敗したら、システム側で一定の期間(元々は数分でしたが、この期間は急激に長くなっています)ログインの試行を禁止します。

これらの攻撃からアプリケーションを守るには、前述のRate Limiterパターンを使いましょう。

正規表現

この手の攻撃は、正規表現の実装によって極端に動作が重くなり、最悪の状況に陥る可能性に付け込んでいます。以下は、不正な正規表現と呼ばれています。

  • 繰り返しのグループ化
  • 繰り返されるグループの内部での
    • 繰り返し
    • オーバーラップによる代替

([a-zA-Z]+)*(a+)+(a|a?)+はは全て脆弱な正規表現です。なぜなら、aaaaaaaaaaaaaaaaaaaaaaaa! のような単純な入力に対して、非常に重いCPU処理をしうるからです。詳細はRegular expression Denial of Service – ReDoSをご覧ください。

自身の正規表現について、これらの内容をチェックするには、safe-regexというツールが便利です。ただし、誤検出されることもあるので注意して使ってください。

$ node safe.js '(beep|boop)*'
true  
$ node safe.js '(a+){10}'
false  

エラー処理

エラーコード、スタックトレース

他のエラー・シナリオでは、X-Powered-By:Expressのような基盤となるインフラストラクチャの機密情報が、アプリケーションから漏れる可能性もあります。

スタックトレースだけでは脆弱性として扱われることはありませんが、攻撃者が興味を持ちそうな情報を明かしてしまうことは頻繁にあります。ある操作でエラーが発生した場合、そのデバッグ情報を提供するのは良いことではありません。常にログは取っておくべきですが、ユーザにはそれを公開しないようにしましょう。

NPM

大いなる力には大いなる責任が伴うものです。NPMは、すぐに使えるパッケージがたくさんある分、コストが高くつきます。それは、「重大なセキュリティの問題が潜んでいるパッケージを使っているかもしれない」というコストです。自分のアプリケーションに必要なものを確認するようにしましょう。

幸い、Node Security Projectには、使用しているモジュールに対して既知の脆弱性をチェックできる素晴らしいツールがあります。

npm i nsp -g  
# either audit the shrinkwrap
nsp audit-shrinkwrap  
# or the package.json
nsp audit-package

requireSafeも役立ちます。

まとめと考察

このリストを作成する上で、OWASPが管理するWeb Application Security Testing Cheat Sheet(Webアプリケーションのためのセキュリティテスト用チートシート)の内容を大いに参考にさせていただきました。

Open Web Application Security Project (OWASP)は、ソフトウェアのセキュリティ向上を目的とした世界規模の非営利組織です。

このリストに不足している内容があれば追加しますので、ご連絡ください。