■■序論
徳丸さんのスライド「
いまさら聞けないパスワードの取り扱い方」に見られるように、昨今、ウェブアプリケーションの設計要件として、
サーバ内に侵入された場合でもユーザーのパスワードをできるだけ保護すべきという論調が見受けられるようになってきました。
上掲のスライドでは、その手法としてソルトつきハッシュ化を勧めています。しかしながらスライドに書かれているとおり、ソルトつきハッシュには、複雑なパスワードの解読は困難になるものの、単純なパスワードを設定してしまっているユーザーのパスワードについては十分な保護を提供できないという問題があります。そして、多くのユーザーは適切なパスワード運用ができない、というのが悲しい現実です。
ソルトつきハッシュを使った手法でこのような問題が残るのは、ウェブアプリケーションサーバに侵入した攻撃者がユーザーの認証情報をダウンロードして、認証情報をオフライン攻撃することを防ぎようがない、という前提を置いているからです。
逆の言い方をすると、
攻撃者がアプリケーションサーバに侵入したとしてもユーザーの認証情報にアクセスできなければ、認証情報を奪われる可能性はないわけです。そのようなシステムを構築するにはどのようにしたらいいでしょうか。
一般的なウェブアプリケーションにおける
答えは、ストアドプロシージャの利用とデータベースサーバの保護にあります
注1。
■■ストアドプロシージャによるパスワード認証
多くのSQLデータベースサーバはストアドプロシージャとストアドファンクションをサポートしており、これはMySQLやPostgreSQLといったオープンソースRDBMSでも例外ではありません。
ストアドプロシージャやストアドファンクションには二つの役割があり、ひとつは処理手順をまとめること、もうひとつは、テーブルへのアクセスパターンを限定することです。
たとえばMySQLの場合、以下のようなDDLを実行することで、
一般ユーザー権限(webapp)からは読み書きできない認証情報カラムpasssalt(ソルト値を格納)とpasshash(ハッシュ値を格納)をもつuserテーブルを作ると同時に、パスワードの一致を確認するストアドファンクション(check_pw)を提供することができます。
-- テーブル定義
CREATE TABLE user (
username varchar(255) NOT NULL,
passsalt varbinary(255) NOT NULL,
passhash varbinary(255) NOT NULL,
PRIMARY KEY (username)
) DEFAULT CHARSET=utf8;
-- パスワードを検証するストアドファンクションを定義
DELIMITER |
CREATE FUNCTION check_pw(u TEXT, p TEXT) RETURNS INT
BEGIN
RETURN (SELECT COUNT(*) FROM user WHERE username=u and passhash=SHA1(CONCAT(passsalt,SHA1(p))));
END;
|
DELIMETER ;
-- パスワードを更新するストアドプロシージャ定義
DELIMITER |
CREATE PROCEDURE update_pw(u TEXT, oldpass NEXT,newpasssalt TEXT,newpasshash TEXT)
BEGIN
DECLARE r INT;
SET r = check_pw(u, oldpass);
IF r = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT 'incorrect old password'
ELSE
UPDATE user SET passsalt=newpasssalt,newpasshash=newpasshash WHERE username=u;
END IF;
END;
|
DELIMETER ;
-- webappユーザーにusernameカラムのみSELECT権限付与
GRANT INSERT,DELETE ON db.user TO webapp@'webapp-host';
GRANT SELECT (username) ON db.user TO webapp@'webapp-host';
-- webappユーザーにストアドの実行権付与
GRANT EXECUTE ON FUNCTION check_pw TO webapp@'webapp-host';
GRANT EXECUTE ON PROCEDURE update_pw TO webapp@'webapp-host';
実際に、このデータベースに一般ユーザー権限で接続してみると、以下のように、
ユーザーの作成はできるものの認証情報は読めない一方で、パスワードの検証は可能、となっていることがわかります。
% mysql -u webapp db
-- 新規ユーザー johndoe (パスワード: johnpass)を作成
mysql> insert into user (username,passsalt,passhash) values ('johndoe','abcdefg',sha1(concat('abcdefg',sha1('johnpass'))));
Query OK, 1 row affected (0.01 sec)
-- userテーブルの全カラムを読むことはできない
mysql> select * from user;
ERROR 1142 (42000): SELECT command denied to user 'webapp'@'webapp-host' for table 'user'
-- usernameカラムだけならば読むことができる
mysql> select username from user;
+----------+
| username |
+----------+
| johndoe |
+----------+
1 row in set (0.00 sec)
-- パスワードの検証は可能
mysql> select check_pw('johndoe','johnpass');
+--------------------------------+
| check_pw('johndoe','johnpass') |
+--------------------------------+
| 1 |
+--------------------------------+
1 row in set (0.00 sec)
-- 間違ったパスワードを指定すると検証に失敗
mysql> select check_pw('johndoe','badpass');
+-------------------------------+
| check_pw('johndoe','badpass') |
+-------------------------------+
| 0 |
+-------------------------------+
1 row in set (0.00 sec)
このように、SQLデータベース内にパスワードの検証/更新ロジックを持たせることで、(データベースに一般ユーザー権限でアクセスするための情報が保存された)ウェブアプリケーションサーバに攻撃者の侵入を許した場合でも、パスワードの漏洩を防ぐことができます。
また、ストアドプロシージャで監査ログを出力することで、攻撃を検知したり被害範囲を特定したりといったことも可能になるでしょう。
(2013/11/27追記: このような手法を実践するにあたっては、
@isidaiさんの指摘をあわせて参照の上、各自の責任において設計、実装くださいますようお願いいたします)
■■データベースサーバの保護
SQLデータベースの機能を利用してパスワードを保護する場合に考えなければならないこととして、ウェブアプリケーションサーバが攻撃者の手に落ちた場合に、そこを踏み台としてデータベースサーバに侵入されるリスクを下げるにはどうすれば良いか、という点があります。
答えは単純で、
データベースサーバをウェブアプリケーションサーバとは別のネットワークセグメントに配置し、両者の間にファイアウォールを設置すれば良い
注2、ということになります。ファイアウォールの役割は、ウェブアプリケーションサーバからデータベースサーバへのアクセスを正規のTCPポートを指定するもの以外、全て遮断することです
注3。
このように設定することで、データベースサーバの権限管理機能にバグがない限り、ウェブアプリケーションサーバを踏み台として認証情報を奪われる可能性はなくなります。
なお、言うまでもありませんが、データベースサーバを配置するネットワークセグメントをインターネットと直接接続してはいけません。理想的には、アクセス手段を厳密に規定して管理系セグメントからのみ接続可能とすべきでしょう。
■■まとめ
ユーザーのパスワードを適切に暗号化することは重要ですが(そのまとめとして徳丸さんのスライドは優れていると思います)、
サーバへの侵入を前提としてアーキテクチャを設計する際には、多層防御の手法が有効になります。
本稿では、
RDBMSサーバのストアドプロシージャとファイアウォールの使用を通じて、ユーザーの認証情報を多層防御するウェブアプリケーションが簡単に構成できることを説明しました。
■■余談
実は、本稿で取り上げた問題とその解は、Unixにおいてシャドウパスワードが導入された経緯の変奏曲です。Unixのシャドウパスワードとは、誰もが閲覧可能なファイル(/etc/passwd)に一方向暗号化(ハッシュ化)されたパスワードを記載しておくのではなく、攻撃者(一般ユーザー)がアクセスし得ないファイル(/etc/shadow)に認証情報を配置し、認証が必要な場合はAPIを通してルート権限で動作するプロセスに問い合わせ、/etc/shadowの内容と比較した結果を回答してもらう、という仕組みです。
注1: 厳密に言うと、RDBMSでなくても別のサーバにAPIで問い合わせるのであればなんでもいいのですが
注2: ウェブアプリケーションとデータベースが1台のサーバに同居するケースでも、OSのユーザベースの権限分離を使って同様のことは不可能ではないと思います
注3: 大規模な構成かのように聞こえるかと思いますが、Amazon AWSのRDSを使うと必然的にこの構成になるなど、実は意外と身近なものです