Memcachedはテキストプロトコルとバイナリプロトコルの二種類を持っています。デフォルトはテキストプロトコルです。テキストプロトコルを利用している場合、テキストインターフェース処理の基本を理解した上で利用しないとセキュリティ問題が発生します。こういった処理のセキュリティ対策を行う、確認するには実は標準の方が簡単で明解 – セキュリティ対策の評価方法も参考になります。
Memcachedはキーバリュー型なのでSQLインジェクションのような脆弱性とは無縁、と思っていた方は是非読んでみてください。
Memcachedのテキストプロトコル
Memcachedのホームページに書いてあるように、memcachedではテキストでコマンドを送り、その結果が返ってきます。
Play with telnet $ telnet localhost 11211 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. get foo VALUE foo 0 2 hi END stats STAT pid 8861 (etc)
改行でコマンドが送られます。 テキストインターフェース処理の基本に書いているように、一文字でも意味のある文字があると要注意です。
※ テキストプロトコルの仕様はGitHubで確認できます。バイナリプロトコルの仕様はWikiで確認できます。
Memcachedのセキュリティ問題
私が詳しく解説するまでもなく、memcachedのセキュリティと脆弱性に詳しく解説されています。概要を紹介します。
Memcachedインジェクション
$memd->get("foo\r\nset bar 0 0 4\r\ntest");
のようなコマンドを送ると、”\r\n”(改行)により別のコマンドが実行され不正なデータが作成できてしまいます。以下のようなログになります。
<30 new auto-negotiating client connection 30: Client using the ascii protocol <30 get foo >30 END <30 set test 0 0 4 >30 STORED <30 connection closed.
メールヘッダーインジェクション、HTTPヘッダーインジェクション(HTTPレスポンススプリッティング)と同じ要領で攻撃です。
現在のphp-memcached(参照していたのはPHP7用開発版、現行版も同じ)は
if (key->len == 0 || strchr(key->val, ' ')) { i_obj->rescode = MEMCACHED_BAD_KEY_PROVIDED; RETURN_FROM_GET; }
のようなコードでこのようなキーに対するインジェクション対策を行っています。’ ‘(半角スペース)にはコマンドを区切る意味があるので’ ‘を検出するだけでコマンドインジェクションが検出できます。”\r”、”\n”も検出する方が良いと思いますが、これでも十分です。
技評さんのサイトでは「サニタイズ」とお勧めしています。仕様として仕方がない場合を除き、私はバリデーションをお薦めします。不正なキーは何らかの攻撃の可能性があり、このような入力はバリデーションエラーとし、攻撃のログを残すようにする方が良いです。
キーの後に改行とコマンドを入れる攻撃だけでなく、異常に長いキーを使って攻撃する方法もあります。
var_dump($m->set(str_repeat('a', 251), "delete foo"));
Memcacheのキーの長さは最大250バイトです。それより大きな場合はエラーになります。キーがエラーになった事により、一緒に送信されたデータがコマンドとして実行されてしまう問題があります。
簡単なPHPコードで確認してみましょう。
<?php define ("MEMC_SERVER_HOST", "127.0.0.1"); define ("MEMC_SERVER_PORT", 11211); function memc_create_instance ($host, $port, array $opts = array (), $persistent_id = null) { $memcached = new Memcached($persistent_id); if ($memcached->setOptions ($opts) == false) echo "Failed to set options" . PHP_EOL; $memcached->addServer($host, $port); $memcached->flush (); return $memcached; } function memc_get_instance (array $opts = array (), $persistent_id = null) { return memc_create_instance(MEMC_SERVER_HOST, MEMC_SERVER_PORT, $opts, $persistent_id); } $m = memc_get_instance (); $m->delete('foo'); var_dump($m->set('foo', 2, 60)); // これはMemcachedで実行されない var_dump($m->set(str_repeat('a', 251), "delete foo")); //これはMemcachedで実行される //var_dump($m->set(str_repeat('a', 250), "delete foo")); var_dump($m->get('foo'));
memcached -vvとして実行したサーバーのログには
<30 new auto-negotiating client connection 30: Client using the ascii protocol <30 flush_all >30 OK <30 delete foo >30 NOT_FOUND <30 set foo 1 60 1 >30 STORED <30 get foo >30 sending key foo >30 END <30 quit <30 connection closed.
と記録されます。不正なdeleteコマンドの実行も、エラーが発生した形跡もありません。memcached+libmemcachedではこの問題は無いようです。(それぞれバージョン1.4.17、1.0.18 で確認。php-memcachedはlibmemcachedを利用)問題のvar_dump()からもFALSEが返ってきていました。
※ 脆弱性データベースを検索してもlibmemcachedの脆弱性は出てこないので、技評サイトで利用していたPerlのMemcachedライブラリが脆弱性である(あった)可能性があると考えられます。
ここまで読むと「値」の方はどうなっているのか?と疑問になると思います。値は
/* If we have compression flag, compress the value */ if (MEMC_VAL_HAS_FLAG(*flags, MEMC_VAL_COMPRESSED)) { /* status */ *payload_len = pl_len; payload = s_compress_value (compression_type, pl, payload_len, flags); }
となっています。compression_typeのデフォルトはphp.iniのmemcached.compression_typeで決まります。デフォルトは”fastlz”なので、デフォルトでは圧縮されます。
圧縮されない場合はどうでしょうか?圧縮されない場合も圧縮した場合も、
status = memcached_set(m_obj->memc, str_key->val, str_key->len, payload, payload_len, expiration, flags);
のようにpayload_len(データの長さ)を指定してMemcachedに渡します。
これで、インジェクションできないことが分かりました。ここまで読むと、「キーバリュー型のDBはインジェクションできない」と思われがちですが、薄氷の上を歩くように安全性が維持されていること解ると思います。仮に今まで安全に動いていたコードでも環境を変えたり、コードを別の言語にポートしたりすると途端に危険になる場合もあり得ることが解ります。
※最後に書いているSQLiteのリンクも参照
アプリケーション開発者はどうすべきか?
php-memcachedの処理は安全なので、特に気にする必要はありません。(Perlを使っている方、要確認かも知れません)しかし、ここで紹介したリスクを知っていたでしょうか?単純/安全に思えるライブラリでも気が付かない所にリスクがあることを理解し、セキュア開発の基本であるデフェンシブプログラミング(防御的プログラミング)を心がけると、思いがけないリスクを低減できます。防御的プログラミングでまず第一に行うべきは確実な入力バリデーションです。
入力バリデーションがより有効に作用するよう、無用に仕様を拡張しないようにすると良いです。例えば、キーとなるような文字列は英数字と”_”のみに限定し、最大64文字までにする、といった仕様にすると入力バリデーションが簡単になると同時により効果的になります。取り扱うデータの大きさも限定的な最大値を決めておき、それ以下になるようにするとトラブルを防止できる可能性が高くなります。
Memcachedはテキストプロトコルなので比較的簡単にライブラリを自作できます。スクリプトベースの実装も多いでしょう。広く利用されていないライブラリは、Memcachedのセキュリティ問題を十分理解しないで作成されている可能性も大きくなります。このようなライブラリは自分でコードを検査してから利用すると良いでしょう。(技評さんサイトのPerlライブラリもスクリプトベースなのかも知れません)
検査するにはこのブログに書いているような事柄を知っていなければなりません。これはテキストインターフェース処理の基本を理解した上で、Memcachedのプロトコル仕様を見れば分かります。
フレームワーク開発者はどうすべきか?
Memcachedはキーバリュー型だからインジェクションできない、と思いこんではいなかったでしょうか?実際にはインジェクションが可能で、下位のライブラリが正しく処理していないと危険です。
アプリケーション開発者であればMemcachedのキーがどのような文字列で構成されるのか?決定したり、知ることができます。しかし、フレームワークやライブラリの開発者はそうも行きません。
最良なのは自分が利用するライブラリ(この例の場合はMemcachedモジュール)の仕様を確認し、安全な仕様なのであればそのままモジュールに処理を任せます。もし、安全でないなら自前で攻撃されないよう、何らかの防御策を導入します。
よく分からないなら、例えば、思い切ってキーのような入力は英数字+”_”だけ、250文字以内(Memcache仕様の最大値)にし、それ以外はエラーにする、などと限定的な仕様にしてしまうと良いです。
もし、プロトコル仕様を参照して独自に実装する場合、仕様をよく読み、全てのの特殊文字に対応し、仕様上の最大値/最小値などを良く理解した上で実装します。コマンドフォーマットをサポートするだけでは全く不十分であることを覚えておきましょう。
管理者はどうすべきか?
Memcachedにはバイナリプロトコルもあります。バイナリプロトコルを使うとテキストインターフェースのような処理にならないので、テキスト処理のリスクは低減できます。telnetなどを使った手軽な確認ができない、などのデメリットもありますが、ライブラリが信用できない場合はバイナリプロトコルを使った方がより安全です。
備考:ただし、バイナリプロトコルはテキストインターフェースに比べバッファーオーバーフロー脆弱性を作ってしまいやすい、といった面もあります。
一般論として必要な知識
いまさら聞けないWebアプリセキュリティの基本ルールで書いたようにWebアプリを作る場合、正確なテキスト管理が必要になります。これには入力と出力を正確に行う必要があります。もしMemcachedのセキュリティ問題を知らなかったとしても、Memcachedのデフォルトプロトコルがテキストベースであることを知り、基礎知識を持った上でセキュリティリスクに対応すれば、今迄になかったような仕組みも安全に利用できます。
特定のプロトコルを安全に取り扱うスキルは全てのWeb開発者に必要なスキルではありません。ワンランク上のWeb開発者を目指す場合には、”正確なテキスト管理がWebセキュリティの基本”は欠かせない知識です。
まとめ
php-memcacheの場合、防御策があります。しかし、同類の問題はMemcachedのインターフェースに限った事ではありません。全ての実装が安全であるとは仮定できません。 もしかすると防御策があっても完全ではないかも知れません。ライブラリや環境が変ると危険になるかも知れません。本来は入出力先の仕様や制限、セキュリティ上の脅威を一つ一つ調べるのが良いのですが、なかなかそうはいきません。
仮に一つ一つ仕様を調べ、出力先へ妥当な出力を行ったとしても出力先のバグなどの影響を受ける場合もあります。一つ一つの入出力先の仕様や制限を詳しく調べる代わりに、厳しめかつ現実的な制限を設け、入力バリデーションでエラーとするか、バリデーションエラーが不可能な場合は入力ミスとして処理すると、このようなリスクを簡単に緩和することが可能になります。
脆弱性発見者に個人で賞金を与えるソフトウェアのqmailやdjbdnsの作者として有名なDaniel Julius Bernstein氏(通称DJB氏)は入力はもちろんlibcを含むライブラリを信用しない、をかなり徹底してコードを書いています。GHOSTのような脆弱性もあるので、セキュアコードを書く方針としては正しいと言わざるを得ないでしょう。
ホワイトリストの方が安全であるのは説明するまでもない、と思っていましたがそうではない、と分かってから、ずっとブログで一貫して伝えています。ホワイトリスト型のバリデーションはブラックリスト型のセキュリティ処理より遥かに強力かつ簡単です。セキュリティ処理は基本的にホワイトリスト型の対策を行います。厳格、制限的なホワイトリスト型の場合、自分のプログラムの出力先や処理仕様を良く理解していなくても、結果的に攻撃可能な脆弱性を作らずに済むことが多くあります。防御的プログラミングの基本要素です。
ネットには科学的、論理的にはおかしなセキュリティ論が沢山あります。 非科学的、非論理的な主張(例えば、ホワイトリスト方式の優位は神話やセキュリティ対策の基準は目的、これでは防御的プログラミング、別名:セキュアプログラミング、は不可能)には惑わされないようにしましょう。
いつもSANS TOP 25ばかり紹介しているのでOWASPの「Secure Coding Practices」(英語PDF)を紹介します。1番目に記述されているのは入力バリデーション(Input Validation)です。2番目は出力エンコーディング(Output Encoding)です。出力エンコーディングと聞くと文字エンコーディングのように聞こえますが、OWASPではエスケープライブラリがエンコーダー(Encoder)と呼ばれています。出力先の仕様に合わせて適切にエンコード(エスケープより広い概念)するという考え方です。
最後に、プリペアードクエリだけ使っていれば少なくともパラメータだけは安全、と思っていた方は以下のブログも参照ください。