Node.js アプリケーションのセキュリティ対策ガイド
開発サイクルの最後に開発者がよく考えることの一つが、アプリケーションの「セキュリティ」です。セキュアなアプリケーションは贅沢品ではなく、必要不可欠なものです。アーキテクチャ、設計、コード、そして最終的にはデプロイメントといった開発の各フェーズでアプリケーションのセキュリティを検討するべきです。
このチュートリアルでは、我々のNode.jsアプリケーションをセキュアにする方法を学んでいきます。さあ、始めましょう。
データ検証 – ユーザーを決して信用しない
システムのユーザーや他のエンティティから来るデータは常に検証またはサニタイズする必要があります。悪い検証、または全く検証しないことは、システムの動作にとって脅威であり、セキュリティエクスプロイトにつながる可能性があります。出力をエスケープすることも重要です。Node.jsで受信データを検証する方法を学びましょう。受信データの検証に validator というノードモジュールを利用できます。例えば、次のように使います。
const validator = require('validator');
validator.isEmail('foo@bar.com'); //=> true
validator.isEmail('bar.com'); //=> false
joi (Codeforgeekによって推奨)と呼ばれるモジュールも使用してデータ/スキーマ検証を行うことができます。例えば、次のように使います。
const joi = require('joi');
try {
const schema = joi.object().keys({
name: joi.string().min(3).max(45).required(),
email: joi.string().email().required(),
password: joi.string().min(6).max(20).required()
});
const dataToValidate = {
name: "Shahid",
email: "abc.com",
password: "123456",
}
const result = schema.validate(dataToValidate);
if (result.error) {
throw result.error.details[0].message;
}
} catch (e) {
console.log(e);
}
SQLインジェクション攻撃
SQLインジェクションは、悪意のあるユーザーが予期しないデータを送り、SQLクエリを変更してしまう脆弱性です。例を使って理解しましょう。想定するSQLクエリは以下のようになっています。
UPDATE users
SET first_name="' + req.body.first_name + '" WHERE id=1332;
通常シナリオでは、このクエリは以下のようになることが期待されます。
UPDATE users
SET first_name = "John" WHERE id = 1332;
しかし、誰かが first_name に以下のような値を渡した場合:
John", last_name="Wick"; --
すると、あなたのSQLクエリは以下のようになります。
UPDATE users
SET first_name="John", last_name="Wick"; --" WHERE id=1001;
ご覧の通り、WHERE条件がコメントアウトされ、クエリはユーザーテーブルのすべてのユーザーの名前を「John」、姓を「Wick」に設定することになります。これは最終的にシステムの故障につながり、あなたのデータベースにバックアップがない場合、あなたは運命です。
SQLインジェクション攻撃を防ぐ方法
SQLインジェクション攻撃を防ぐ最も有効な方法は、入力データのサニタイズです。すべての入力を検証するか、パラメータバインディングを使って検証します。パラメータバインディングは効率性とセキュリティを提供するため、多くの開発者によって使われます。sequelizeやhibernateなどの有名なORMを使用している場合、彼らは既にデータを検証しサニタイズする関数を提供しています。ORM以外のデータベースモジュール、例えば Nodeのためのmysqlを使用している場合は、モジュールによって提供されるエスケープメソッドを使用できます。以下の例コードを見てみましょう。ここではNode用の mysql モジュールを使用しています。
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
});
connection.connect();
connection.query(
'UPDATE users SET ?? = ? WHERE ?? = ?',
['first_name',req.body.first_name, ,'id',1001],
function(err, result) {
//...
});
二重の疑問符はフィールド名に、単一の疑問符は値に置き換わります。これによって入力が安全になります。セキュリティレベルを高めるためにストアドプロシージャを使用することもできますが、メンテナンス性が低いため、開発者はストアドプロシージャの利用を避ける傾向にあります。
型キャスティング
JavaScriptはダイナミックに型付けされた言語です。つまり、値は任意の型になる可能性があります。型キャスティング方法を使用して、データベースに入れるデータの型を確認することで、想定された型の値のみがデータベースに入るようにすることができます。たとえば、ユーザーIDは数値のみを受け入れることができるので、ユーザーIDが数値のみであることを確実にするために型キャスティングを行うべきです。例を見てみましょう。
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'me',
password : 'secret',
database : 'my_db'
});
connection.connect();
connection.query(
'UPDATE users SET ?? = ? WHERE ?? = ?',
['first_name',req.body.first_name, ,'id',Number(req.body.ID)],
function(err, result) {
//...
});
変更に気付きましたか?IDが常に数値であることを保証するために Number(req.body.ID) を使用しました。この美しい 記事を参考にして、JavaScriptでの型キャスティングを深く理解してください。
アプリケーション認証と承認
パスワードのような機密データは、悪意のあるユーザーが機密情報を悪用しないように、システム内で安全に格納されなければなりません。このセクションでは、非常に一般的で、ほとんどのアプリケーションがシステム内で何らかの形でパスワードを持っているため、パスワードの格納と管理方法について学びます。
パスワードのハッシュ化
ハッシュ化は、入力から固定サイズの文字列を生成する機能です。ハッシュ化関数からの出力は復号化できないため、「片道」の性質を持ちます。パスワードのようなデータについては、常にハッシュ化アルゴリズムを使用して、平文の入力パスワード文字列からハッシュ版を生成する必要があります。
ハッシュが片道の文字列であるなら、どうして攻撃者はパスワードにアクセスできるのでしょうか?
上述したように、ハッシュ化は入力文字列から固定長の出力を生成します。それで攻撃者は逆のアプローチを取り、一般的なパスワードリストからハッシュを生成し、そのハッシュをあなたのシステム内のハッシュと比較してパスワードを見つけ出します。この攻撃は ルックアップテーブル 攻撃と呼ばれます。
このために、システムのアーキテクトとして、一般的なパスワードをシステム内で許可してはいけません。この攻撃を克服するために、"salt" を使うことができます。Saltは入力に関係なく、パスワードハッシュに固有のものにするために付けられます。Saltは予測不能なように、安全でランダムに生成されなければなりません。私達がお勧めするハッシングアルゴリズムは BCrypt です。この記事を書いている時点で、Bcryptは侵害されておらず、暗号学的に安全だと考えられています。Node.jsでは、bcrypt ノードモジュールを使用してハッシングを行うことができます。
以下の例コードを参照してください。
const bcrypt = require('bcrypt');
const saltRounds = 10;
const password = "Some-Password@2020";
bcrypt.hash(
password,
saltRounds,
(err, passwordHash) => {
//今のところコンソールに表示するだけです
//どこかに保存するべきで、ログに記録したり印刷するべきではありません
console.log("Hashed Password:", passwordHash);
});
SaltRounds 関数はハッシュ関数のコストです。コストが高いほど、より安全なハッシュが生成されます。サーバーの計算能力に基づいて塩を決定する必要があります。パスワードのハッシュが生成されたら、ユーザーが入力したパスワードはデータベース内に保存されているハッシュと比較されます。参考のために、以下のコードを見てください。
const bcrypt = require('bcrypt');
const incomingPassword = "Some-Password@2020";
const existingHash = "some-hash-previously-generated"
bcrypt.compare(
incomingPassword,
existingHash,
(err, res) => {
if(res && res === true) {
return console.log("Valid Password");
}
//不正なパスワードの処理はこちら
else {
console.log("Invalid Password");
}
});
パスワードの格納
データベースやファイルを使用してパスワードを格納する場合でも、平文バージョンを格納してはいけません。上述したように、ハッシュを生成してシステム内に格納する必要があります。一般的に varchar(255) データ型をパスワードの場合に推奨します。また、フィールドの長さを無制限にすることもできます。bcrypt を使用している場合は、bcrypt が固定サイズの60文字のハッシュを生成するため、varchar(60) フィールドを使用できます。
承認
適切なユーザーロールと権限を持つシステムは、悪意のあるユーザーがその権限を超えて行動することを防ぎます。適切な承認プロセスを達成するためには、各ユーザーに適切な役割と権限が割り当てられ、それ以上のことは行えません。Node.jsでは、システム内の承認に基づいてアクセス制御リストを開発するために、ACLと呼ばれる人気のあるモジュールを使用できます。
const ACL = require('acl2');
const acl = new ACL(new ACL.memoryBackend());
// ゲストはブログの閲覧を許可されている
acl.allow('guest', 'blogs', 'view')
// その権限が付与されているかチェック
acl.isAllowed('joed', 'blogs', 'view', (err, res) => {
if(res){
console.log("ユーザーのjoedはブログを閲覧することができます");
}
});
より多くの情報と例のコードについては、acl2のドキュメントをチェックしてください。
ブルートフォース攻撃の予防
ブルートフォースは、ハッカーがソフトウェアを使って異なるパスワードを繰り返し試し、アクセスが許可されるまでつまり有効なパスワードが見つかるまで実行する攻撃です。ブルートフォース攻撃を予防する最も単純な方法の一つは「待つ」アプローチです。誰かがあなたのシステムにログインしようとして3回以上無効なパスワードを試みた場合、再度試行する前に60秒ほど待つようにさせます。これにより攻撃者は遅くなり、パスワードをクラックするためには永遠にかかるでしょう。
それを予防する別のアプローチは、無効なログインリクエストを生成するIPを禁止することです。システムは24時間に3回間違った試行を各IPに許可します。誰かがブルートフォースを試みる場合、そのIPを24時間
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/shaikhshahid/a-guide-to-securing-node-js-applications-4bcc