2015年5月18日月曜日

『最初に「読む」PHP』は全体的にとても良いが惜しい脆弱性がある

最初に「読む」PHP(クジラ飛行机)を読みました。本書にはセキュリティ用語(クロスサイトスクリプティング、SQLインジェクション等)はほとんど出てきませんが、脆弱性についてよく配慮された記述となっています。しかし、その細部の詰めが甘く、脆弱性が混入してしまいました。その内容を報告したいと思います。

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

出力時にHTMLエスケープするという原則を比較的早い段階で説明しています。その説明が素晴らしいと思いました。
ユーザーから送信されたデータを、PHPを使ってそのまま画面に表示することは、レイアウトの崩れや セキュリティ上の危険につながります。必ず、HTMLに変換してから表示します。そこで、画面に「(^o^)<Hello」と表示するプログラムを作ってみましょう。
「HTMLに変換して」という表現がとてもいいですね。難しくいうと、Content-Typeをtext/plainからtext/htmlに変換するということですが、平たくいうと、(プレーンテキスト文字列を)HTMLに変換するということです。
そして、以下のサンプルプログラムが続きます。
<?php
$s = "(^o^)<hello";
echo $s;
?>
実行結果は以下の通りです。「<Hello」がタグとみなされて消えていますね。


このように、htmlspecialcharsというのは、文字列をHTMLに変換するものという説明は本質をついているように思います。そして、本書には「クロスサイトスクリプティング」という用語は出てきませんが、作成するアプリケーションにはクロスサイトスクリプティング脆弱性が入り込まないように配慮して説明されています。
ただし、残念ながら細かい詰めが甘いため、本書で紹介されているスクリプトにはクロスサイトスクリプティング脆弱性が含まれますが、それについては後述します。

SQLインジェクション

PHP入門書としては珍しくエスケープとプレースホルダ(プリペアードステートメント)の両方が説明されています。まずはエスケープですが、以下のように説明されています。
次に、データベースにデータを挿入します。exec()メソッドでSQLを実行するという点は同じなのですが、ここで1つ注意したいことがあります。それは、PHPでSQLを実行するときには、SQLは文字列で指定しなくてはならないという点です。
そのため、挿入する値の中に、SQLの意味を変えてしまうような値(例えば、文字列を表す引用符「'」や「"」)があると、開発者の意図に反したSQLになってしまうことがあります。
そこで、データベースに値を挿入する際には、$db->quote()メソッドを使って、あらかじめ、文字列に含まれる引用符「'」や「"」をエスケープしておきます。
「SQLインジェクション」という用語を用いずに、エスケープの必要性を説明しています。本来こうあるべきですよね。私はこの説明の方針に賛同します。エスケープのスクリプトは以下の通りです。
$item_id = intval($i["item_id"]);
$name    = $db->quote($i["name"]);
$price   = intval($i["price"]);
$result = $db->exec(
    "INSERT INTO items (item_id, name, price)".
    "VALUES($item_id, $name, $price)");
整数列はintval関数で整数に変換し、文字列型の列はPDOのquoteメソッドにより、エスケープした文字列をクォートしています。この書き方は間違える要素は少ないので、エスケープ手法を取る場合には良い方法だと思います。

※ 本来、quoteメソッドで整数リテラルも作成できるべきなのですが、PDOのquoteメソッドはできがよくない(参考)ので、文字列に限定して使うのは賢明です。

その後、プレースホルダ(プリペアードステートメント)を説明し、本番のスクリプトはプレースホルダで書いています。
ところが、人間はうっかりするのが得意な生き物です。うっかりすると、クォート処理を書き忘れる可能性があります。
そこで、SQLのひな型を先に書いておいて、そこに値を当てはめる方法でSQLを実行する「プリペアードステートメント」という機能が用意されています。プリペアードステートメントを使うと、ひな型をあらかじめコンパイルしておいて、そこに後から値を差し込むことができます。また、自動的に面倒なクォート処理を行ってくれるので、とても便利です。
微妙に説明がおかしい(動的プレースホルダと静的プレースホルダの説明が混ざっている)のが気になりますが、説明の方針としては良い感じです。その後、本書では一貫してプレースホルダによりSQLを呼び出しているので、本書内にSQLインジェクション脆弱性はないようです。

本書ではトランザクションの説明もある

以前ブログ記事「嵐のコンサートがあるとダブルブッキングしてしまうホテル予約システムを作ってみた」にて私は以下のように書きました。
PHP入門書ないし中級レベルの解説書では、通常トランザクション処理や排他制御の解説はありません。
ところが本書は、「最初に『読む』」といううたい文句の本であるにも関わらず、トランザクションの説明があります。素晴らしいですね。
トランザクション(transaction)とは、分けることのできない一連の情報処理の単位です。このサランザクションを理解するには、銀行の決済処理を考えると分かりやすいと思います。
例えばAさんがBさんんの口座へ50万円の振り込みを行うとします。このとき銀行システムが行うのは、次の2つの動作です。

(1)Aさんの口座から50万円減らす
(2)Bさんの口座を50万円増やす

このとき、もし、Aさんの口座から50万円を減らした時点で何かしらのトラブルが起きて、決済処理が止まってしまったとします。すると、Bさんの口座に振り込まれていないにも関わらず、Aさんの口座だけが減っているという、困った状況が発生してしまいます。
これを避けるためにトランザクションが存在します。つまり、一連の振り込み処理を最終的に完了してはじめて、実際にデータベースの値が変更されるという仕組みなのです。
上記の処理ではトランザクションに加えて行ロックなどによる排他制御も必要ですが、それは説明されていません。そこは残念ですが、入門書の範囲を超えるということなのでしょう。本書の読者は、本書をきっかけとしてトランザクションなど本格的なSQLの勉強に進むことを期待します。

本書の残念なところ

本書のSQLインジェクション対策は十分なようですが、クロスサイトスクリプティングについては抜けがあり、エスケープの詳細に問題があります。それは、属性値の扱いです。
本書では、素のHTMLでは属性値をダブルクォートで囲み、PHPスクリプトではシングルクォートで囲んでいます。属性値をシングルクォートで囲む場合、HTMLエスケープの際にシングルクォートをエスケープするためにhtmlspecialcharsでENT_QUOTESを指定する必要がありますが、それが抜けています。
下記は、P141のselect-color.phpというスクリプトの一部で、クエリ文字列colorにより背景色を選択するというものです。
// 色が送信されているか調べて背景色を決定する
$color = "#FFFFFF";          // デフォルトは白色
if (isset($_GET["color"])) { // 色が送信されているか?
  $color = htmlspecialchars($_GET["color"]);
}
echo "<html><body bgcolor='$color'>";        // HTMLのヘッダを表示
ご覧のように、属性値bgcolorをシングルクォートで囲っているにも関わらず、htmlspecicalcharsにENT_QUOTESが指定されていません。そのため、colorとして、下記の文字列を指定するとクロスサイトスクリプティング攻撃ができます。
'+onload%3d'alert(1)
この結果生成されるHTMLは下記の通りです。
<html><body bgcolor='' onload='alert(1)'>
表示は下記のようになり、JavaScriptが実行されます。


これを避けるには、以下のいずれか(両方でもよい)を実施する必要があります。
  • 属性値をダブルクォートで囲む
  • htmlspecialcharsの第2引数にENT_QUOTESを指定する

まとめ

『最初に「読む」PHP』の脆弱性対処の状況について説明しました。一言で言うと、以下の感想です。
  • セキュリティの専門用語を使わずに脆弱性対処を自然な形で説明するという全体方針はとてもよい
  • 属性値の扱いでクロスサイトスクリプティング脆弱性が入ってしまったのは痛恨の極みだ
入門書に対して細かいことを言うと思われるかもしれませんが、セキュリティは細かいことが重要なのです。本書は、全体としてはとても良くかけていて、セキュリティに関しても注意深く取り扱われています。それだけになおのこと、この記事で指摘した「漏れ」で脆弱性が入ってしまったことはとても残念に思いました。
一方、SQLインジェクションについては特に問題なく、こちらの記事で書いたように、以下の状況が継続しています。
PHPの入門書ではSQLインジェクション脆弱性がないことが当たり前になった
これは、PDOでプレースホルダを使うという、PHPのSQLインジェクション対策の決定版といえる書き方が普及したからと言え、とても好ましいと感じました。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年5月11日月曜日

セキュアなWEBサイト運用のためのワークショップにて講演します

キヤノンITソリューションズ株式会社主催のセミナー「セキュアなWEBサイト運用のためのワークショップ~ハッカーが攻撃をあきらめるWEBサイトとは?~」にて講演します。

日時:2015年5月27日(水)13:30~17:00 受付は13:00より
場所:キヤノンマーケティングジャパン株式会社 大阪支店 18F(大阪市北区 地図
費用:無料(申し込みはこちら
講演タイトル:やぶられにくいWEBサイトの作り方  ~被害事例から学ぶ攻撃手法とその対策~

企業セミナーとしては長めの80分という枠をいただいていますので、具体的な攻撃手法をたっぷりお見せしたいと思います。「被害事例から学ぶ…」というタイトルですので、具体的な事例の手法(推測含む)の攻撃手法のデモをお見せします。

  • 情報通信研究機構(NICT)ウェブサイトに対する侵入事件(Joomlaの脆弱性)
  • メガネ通販サイトに対する侵入事件(Struts2の脆弱性によりサイト改ざん、入力内容窃取)
  • EC-CUBEカスタマイズ部分のSQLインジェクションによる個人情報窃取
  • パスワードリスト攻撃による不正ログイン
  • なりすまし犯行予告(CSRF攻撃)
  • 一行掲示板に対するワーム(XSS)
  • その他

また、XSSのデモなど見飽きた言う方も多いだろう一方で、「XSSの何が悪いのかわからない」という意見も目にするところですので、XSSを用いて、私手作りのJavaScriptワームを掲示板上で増殖させてみたいと思います。

それでは、よろしくお願いいたします。

2015年5月8日金曜日

「いちばんやさしいPHPの教本」のレビューを担当しました

柏岡 秀男さん、池田 友子さん共著の「いちばんやさしいPHPの教本 人気講師が教える実践Webプログラミング」のレビューを担当いたしました。
柏岡さんとは面識がありませんでしたが、メールにて依頼をいただき、お受けするかどうか迷ったので「ゲラを見せていただいたからお返事します」と答えたところ、さっそくに送付いただき、それを確認した上でお引き受けいたしました。

見せていただいた原稿は元々セキュリティにはしっかり配慮されて書かれていましたが、細かいところの指摘を何点かさせていただきました。例えばエラーメッセージの扱いなどですね。指摘には快く対応いただき、ありがとうございました。

目次は以下の通りです。
  1. PHPを学ぶ準備をしよう
  2. プログラムを作りながらPHPの基本を学ぼう
  3. データベースを作成しよう
  4. データベースと組み合わせたプログラムを作ろう
同書の特徴ですが、「いちばんやさしい」と表題にある通り、あまり機能を欲張らずにSQLによるCRUD(新規作成、読み出し、更新、削除)がひと通りできるくらいまでを丁寧に説明しています。題材としては料理レシピのデータベース(認証なし)を用いています。

同書を読んでいて面白いと思ったのは、コードの入力方法です。例えば以下は同書P82からの引用ですが…


まず一旦body要素を閉じていますね。そして、body要素の中身を次に入力していきます。以下は同書P83からの引用です。一旦form要素を入力してそれを閉じています。form要素の中身は次ページ以降で入力しています。


このように、初めてPHPを動かす読者を想定して細やかな配慮がなされています。

動作環境はWindows上のXAMPPあるいはMac上のMAMPを用い、エディタ(さくらエディタ、CotEditor)も含めてインストールから設定まで詳しく解説しています。

実は、この種の初心者向けPHP入門書の最近のトレンドは、ファイルのアップロードやメール送信、認証など盛りだくさんの機能が入っているものですが、同書はそこまで欲張らずに基本的なところにとどめています。これは正解だと思います。とくに、ファイルアップロードや認証はセキュリティ要件が厳しいので、中級者向けのテキストで扱うのがよいと思います。

本書で基礎的なPHPの機能を学習した後には、同じく入門書ではありますが、「10日でおぼえるPHP入門教室 第4版」に進むのがよいと考えます。「10日で覚える~」の方は、入門教室とある割にはやや高度で(Amazonレビューからも分かります)、機能的に充実していて、セキュリティの配慮も十分だからです。
その後は、パーフェクトPHPPHP逆引きレシピ 第2版でPHPの理解を深めていくという学習シナリオが考えられます。そうそう、できれば拙著もご活用下さいw


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年5月7日木曜日

嵐のコンサートがあるとダブルブッキングしてしまうホテル予約システムを作ってみた

今年の5月1日に、仙台市内のホテルで多重予約のトラブルが発生したと報道されています。
部屋数203室の仙台市のビジネスホテルで、9月18~23日の宿泊予約を数千件受け付けるトラブルがあった。アイドルグループ「嵐」のライブが宮城県内で開催される期間だった。インターネットでの申し込みが殺到し、システム障害が起きたとみられるという。

 トラブルがあったのは、仙台市泉区の「ホテルルートイン仙台泉インター」。ホテルなどによると、9月19、20、22、23日に宮城スタジアム(宮城県利府町)で嵐がライブを開くことが明らかになった後の5月1日午前5時ごろ、ネットを使った予約申し込みが殺到していることに気づいたという。

203室のホテルなのに「予約」数千件 嵐公演で殺到か:朝日新聞デジタル より引用
5月1日の朝に何があったのか調べてみると、この日の早朝にテレビや新聞でコンサートの情報が流れたようですね。
お嬢様方~おはようございます!
今朝の4時~LINEがどえらい騒ぎで
結局ねてません(笑)

皆さんお知らせありがとうこざいます♥
東北でコンサートみんな参加できたらいいですね♪

嵐色の小部屋 嵐さん仙台でコンサート決定【追記】 より引用
つまり、こういうことのようです。
  • 今年9月に仙台で嵐のコンサートが実施されると、5月1日早朝にテレビなどが報道
  • 嵐ファンの間でLINE等で情報が展開される
  • 仙台近隣のホテル予約に殺到
  • 当該ホテルがシステム障害を起こし、多重予約に至る
では、このような多重予約がなぜ発生してしまったかですが、その原因は公表されていないので憶測するしかありません。「ありそうな可能性」としては、排他制御が十分でなかったのではないかという仮説が成り立ちます…ということで、表題のように、「嵐のコンサートがあるとダブルブッキングしてしまう予約システム」を作ってみました。

予約システムの概要

まずはテーブル定義です。rooms_availableは宿泊日と客室タイプ毎の空き部屋数、transaction_logは予約の履歴を保持しています。
CREATE TABLE `rooms_available` (
  `id` int(11) NOT NULL AUTO_INCREMENT,  /* id */
  `date` date NOT NULL,                  /* 宿泊日 */
  `room_type` int(11) NOT NULL,          /* 客室タイプ */
  `available` int(11) NOT NULL,          /* 空き室数 */
  `reserved` int(11) NOT NULL,           /* 予約済み室数 */
  PRIMARY KEY (`id`),
  UNIQUE KEY `date_type` (`date`,`room_type`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;

CREATE TABLE `transaction_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,  /* id */
  `date` date NOT NULL,                  /* 宿泊日 */
  `room_type` int(11) NOT NULL,          /* 客室タイプ */
  `customer` int(11) NOT NULL,           /* 顧客番号 */
  `rooms` int(11) NOT NULL,              /* 予約室数 */
  PRIMARY KEY (`id`),
  UNIQUE KEY `date_room_cust` (`date`,`room_type`,`customer`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
続いて、客室予約のスクリプトです。宿泊日と顧客番号(整数)を指定して、予約を申し込むものです。処理の性質上POSTリクエストにすべきものですが、テストの都合でGETにしています。
<?php
  $date = $_GET['date'];          // 宿泊日
  $room_type = 1;                 // 客室タイプは1固定に
  $customer = $_GET['customer'];  // 顧客番号

  try {
    $dbh = new PDO("mysql:host=localhost;dbname=db;charset=utf8", "dbuser", "password");
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // 当該日の空室を求める
    $sth = $dbh->prepare("SELECT available FROM rooms_available WHERE date=? AND room_type=?");
    $sth->bindValue(1, $date, PDO::PARAM_STR);
    $sth->bindValue(2, $room_type, PDO::PARAM_INT);
    $sth->execute();
    $available = 0;
    if ($row = $sth->fetch()) {
       $available = (int)$row['available'];
    }

    if ($available > 0) { // 空室ありの場合
      // 取引ログに記録
      $sth = $dbh->prepare("INSERT INTO transaction_log(date, room_type, customer, rooms) VALUES(?, ?, ?, 1)");
      $sth->bindValue(1, $date, PDO::PARAM_STR);
      $sth->bindValue(2, $room_type, PDO::PARAM_INT);
      $sth->bindValue(3, $customer, PDO::PARAM_STR);
      $sth->execute();

      // 空室情報を更新
      $available--;  // 空室数を1減じてDBに書き戻す。同時に予約数を1増やす
      $sth = $dbh->prepare("UPDATE rooms_available SET available=?, reserved=reserved+1 WHERE date=? AND room_type=?");
      $sth->bindValue(1, $available, PDO::PARAM_INT);
      $sth->bindValue(2, $date, PDO::PARAM_STR);
      $sth->bindValue(3, $room_type, PDO::PARAM_INT);
      $sth->execute();

      echo "ご予約を承りました";
    } else { // 満室の場合
      header("HTTP/1.1 400 Bad Request");  // 監視の都合でステータス400とする
      echo "恐れ入りますがただいま満室でございます";
    }
  } catch (PDOException $e) {
    header("HTTP/1.1 500 Internal Server Error");  // 監視の都合でステータス500とする
    error_log($e->getMessage());  // エラー詳細はログに出力
    echo 'ただいまアクセスが集中しております。しばらくたってからアクセスしてください';
  }
処理の大まかな流れは下記の通りです。
  • 宿泊日の空室数を取得($available)
  • 空室数が0より大きければ以下を行い、0の場合は満室と表示して終了
    • 宿泊成立として取引ログに宿泊内容を書き込む
    • 空室数を 1 減らし、予約済み室数を 1 増やす

テスト条件

当該のホテルは203室ということなので、条件を簡単にするために203室が全て同じタイプであり、この事故の前には予約は 0 だったと仮定します。rooms_availableテーブルの状態は下記のとおりです。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |       203 |        0 |
|  2 | 2015-09-19 |         1 |       203 |        0 |
|  3 | 2015-09-20 |         1 |       203 |        0 |
|  4 | 2015-09-21 |         1 |       203 |        0 |
|  5 | 2015-09-22 |         1 |       203 |        0 |
|  6 | 2015-09-23 |         1 |       203 |        0 |
+----+------------+-----------+-----------+----------+
この状態で負荷テストツール siege を用いて、同時接続 200 で連続的に予約を行い、嵐ファンの予約の様子をシミュレーションしました。siegeを用いた理由は、リクエスト毎にパラメータをファイル指定で変化させやすいからです。

結果

負荷テストの模様を映像で紹介します。画面上半分がsiegeを実行している様子。下半分は宿泊日毎の残室と予約済室数のモニタです。



負荷テスト後の各テーブルの状態は下記の通りで、合計 1,218件(203×6)の予約しか受け付けられないはずのところ、約4,400件の予約が成立しました。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |         0 |      719 | ← 各日とも700件超の予約が入っている
|  2 | 2015-09-19 |         1 |         0 |      766 |
|  3 | 2015-09-20 |         1 |         0 |      712 |
|  4 | 2015-09-21 |         1 |         0 |      735 |
|  5 | 2015-09-22 |         1 |         0 |      740 |
|  6 | 2015-09-23 |         1 |         0 |      715 |
+----+------------+-----------+-----------+----------+

mysql> SELECT SUM(reserved) FROM rooms_available;
+---------------+
| SUM(reserved) |
+---------------+
|          4387 | ← 延べ1,218室のはずが、予約総室数は 4,387に
+---------------+

mysql> SELECT COUNT(*) FROM transaction_log;
+----------+
| COUNT(*) |
+----------+
|     4387 | ← 取引ログの総数も 4,387 件に
+----------+

多重予約の原因

多重予約が発生してしまう原因は、排他制御の不備にあります。以下は、リクエストAとリクエストBが同日の宿泊予約をほぼ同時に要求している場合の模式図です。


上図のように、空室数を同時に(並行して)求めて、それぞれ独自に空室判定をしているところに問題があります。同じ日付の空室については、1つのスレッドのみが判定をしなければ多重予約が起きる可能性があります。つまり、空室数を求めてから、空室数をアップデートするまでは排他的に処理を行う必要があります。このような箇所はクリティカルセクションと呼ばれます。

対策

上記現象の対策としては排他制御をきちんとするということになりますが、MyISAMはトランザクションに対応していないので、対策方針としては以下の二案が考えられます。
  • MyISAMのままテーブルロックを使用する
  • ストレージエンジンをInnoDBに変更して、トランザクションと行ロックを使用する
どちらでも対策は可能ですが、予約システムは更新処理が多いという特性から、InnoDBとトランザクションを用いるほうが一般的でしょう。

ストレージエンジンをInnoDBに変更

トランザクション処理を利用するためには、ストレージエンジンとしてMyISAMではだめで、InnoDBを用いる必要があります。このため、CREATE TABLE文のENGINE=MyISAMとなっている2箇所をENGINE=InnoDBに変更します。

トランザクションの使用と行ロック

次に、トランザクションの使用と行ロックを指示します。PDO+MySQLの組み合わせの場合、デフォルトではSQLの呼び出しのたびにトランザクションを開始して自動的にコミットされます(オートコミット)。これでは排他制御ができないので、beginTransaction()メソッドで明示的にトランザクションを開始して、commit()メソッドでコミットします。また、SELECT文に「FOR UPDATE」を指定することで、選択された行に対して行ロックを行います。予約ができなかった場合や例外を捕捉した場合は、ロールバックにより処理を取り消します。
行ロックはコミットあるいはロールバックにより解除されます。
  $dbh = new PDO("mysql:host=localhost;dbname=db;charset=utf8", "dbuser", "password");
  $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  $dbh->beginTransaction();  // トランザクションの開始

  $sth = $dbh->prepare("SELECT available FROM rooms_available WHERE date=? AND room_type=? FOR UPDATE"); // 行ロック
  // 中略

    $dbh->commit(); // コミット
    echo "ご予約を承りました";
  } else {
    header("HTTP/1.1 400 Bad Request");  // 監視の都合でステータス400とする
    $dbh->rollBack();  // ロールバック
    echo "恐れ入りますがただいま満室でございます";
  }
} catch (PDOException $e) {
  header("HTTP/1.1 500 Internal Server Error");
  $dbh->rollBack();  // ロールバック
  error_log($e->getMessage());
  echo 'ただいまアクセスが集中しております。しばらくたってからアクセスしてください';
}
改修後の実行結果は以下の通り、各日の予約数は203となり、ダブルブッキングは生じなくなりました。
mysql> SELECT * FROM rooms_available;
+----+------------+-----------+-----------+----------+
| id | date       | room_type | available | reserved |
+----+------------+-----------+-----------+----------+
|  1 | 2015-09-18 |         1 |         0 |      203 |
|  2 | 2015-09-19 |         1 |         0 |      203 |
|  3 | 2015-09-20 |         1 |         0 |      203 |
|  4 | 2015-09-21 |         1 |         0 |      203 |
|  5 | 2015-09-22 |         1 |         0 |      203 |
|  6 | 2015-09-23 |         1 |         0 |      203 |
+----+------------+-----------+-----------+----------+
この際の処理の流れは以下のように並行ではなく、排他的な処理になり、一貫性は維持されます。


PHP入門書での対応

PHP入門書ないし中級レベルの解説書では、通常トランザクション処理や排他制御の解説はありません。
パーフェクトPHPWebアプリケーション構築入門(第2版)には、トランザクションの簡単な説明がありますが、具体例などは説明されていないようです。
改訂版 今すぐ導入!PHP×PostgreSQLで作る最強Webシステムにはトランザクションの詳しい説明がありますが、これは著者の石井達夫氏がPosgreSQLの専門家なので当然かもしれません。

まとめ

嵐のコンサート情報が引き起こしたホテル多重予約問題を題材として、トランザクションと排他制御の重要性について説明しました。
当該の事故の原因はおそらくもっと複雑なものであろうと予想しますが、ここで説明したレベルの単純な排他制御であってもPHPの入門書等では解説されていないので、初めて知ったという読者も多いのではないでしょうか。
ホテル予約のダブルブッキングがセキュリティ上の問題かといえば、広義に捉えればセキュリティ上の問題と言えると思いますが、排他制御不備が個人情報漏洩等につながる場合もあり、セキュリティに直結する場合もあります。このため、拙著4.15節では、「共有資源に関する問題」として(SQLの問題ではありませんが)排他制御の重要性について説明しています。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年5月2日土曜日

エラーメッセージによるXSSにご用心

以前の記事「PHPのdisplay_errorsが有効だとカジュアルにXSS脆弱性が入り込む」では、php.ini等でdisplay_errorsを有効にしていると、スクリプトに脆弱性がなくてもXSS(クロスサイトスクリプティング)脆弱性が入り込む可能性が高いことを指摘しました。
しかし、display_errorを無効にしていても、エラー処理がまずいと、エラー表示が原因でXSS脆弱性が入り込む場合があります。ネット上のサンプルスクリプトを見ても、潜在的にXSS脆弱性があるものが多くあります。

サンプルスクリプト

まずは、典型的な脆弱性の例をスクリプトで紹介します。PHP+PDO+PostgreSQLの組み合わせです。
<?php
try {
  $db = new PDO("pgsql:host=localhost options='--client_encoding=UTF8';dbname=test",
    DBUSER, DBPASS);
  $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); // 例外を有効に設定
  $ps = $db->prepare("INSERT INTO users VALUES(?, ?)");
  $ps->bindValue(1, $_GET['id'], PDO::PARAM_INT);
  $ps->bindValue(2, $_GET['name'], PDO::PARAM_STR);
  $ps->execute();
  echo '登録しました';
} catch (PDOException $e) {
  die($e->getMessage());
}
ご覧のように、PostgreSQLのデータベースに接続して、id(整数)とname(文字列)を挿入するスクリプトです。

エラーの表示例

エラー表示の例を示します。下記は、整数列であるidに「xxx」を指定した場合の画面表示です。「 invalid input syntax for integer: "xxx"」と表示されていますが、入力値が「そのまま」表示されているところに嫌な予感がしますね。


XSS

下記は、idにJavaScriptを指定した場合の画面表示です。ご覧のように、JavaScriptが起動しています。XSS脆弱性が混入してしまいました。


この際のHTMLソースは以下の通りです。
SQLSTATE[22P02]: Invalid text representation: 7 ERROR:  invalid input syntax for integer: "<script>alert(1)</script>"
エラーメッセージをHTMLエスケープしないで表示していることが原因です。

対策

本来、システム内部的なエラー内容は画面に表示すべきではありません。利用者にとっては不必要な情報であり、かつ攻撃のヒントが得られる場合があるからです。このため、エラーの詳細情報は画面表示せず、ログファイルに出力するようにします。
一方、なんからの事情があってエラー情報を画面表示する場合は、表示の前にHTMLエスケープすべきです。
例: echo htmlspecialchars($e->getMessage(), ENT_QUOTES, 'UTF-8');

まとめ

エラーメッセージに起因するXSSについて説明しました。
Google検索で「PDOException getMessage」で検索すると、エラー詳細をHTMLエスケープしないで表示しているサンプルが大半です。学習目的あるいはデバッグ用という意図かもしれませんが、サンプルには明示されていないため、初学者はそれが「正しい書き方」と思うのではないでしょうか。
そもそもエラー詳細を画面表示しないことが原則ですが、説明等の目的でエラー内容を表示する場合は、常に正しいスクリプトを示すという意味からHTMLエスケープを忘れないようにいたしましょう。


【HASHコンサルティング広告】
HASHコンサルティング株式会社は、セキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年4月30日木曜日

Apacheの多重拡張子にご用心

先日の日記『「10日でおぼえるPHP入門教室 第4版」はセキュリティ面で高評価』では、同書のアップロード機能のセキュリティ面を評価しつつ、「もうひと踏ん張り確認して欲しい内容がある」として、画像XSSの可能性について指摘しました。では、これを直せば完璧かというと、実はそうとも言えないという微妙な問題があります。それは、アップロード先の場所とファイル名の問題です。
ファイルをアップロードするディレクトリ: ドキュメントルート下の /php10/doc/
ファイル名: ブラウザから送信されたファイル名そのまま
これらのうちファイル名の拡張子については、gif/jpg/jpeg/pngのみを許すという、いわゆるホワイトリスト検査がされていて、またgetimagesize()関数により、画像ファイルであることの簡易的なチェックをしています。しかし、この状態では、環境によってはアップロードしたファイルをPHPスクリプトとして実行される危険性があります。

同書は、その種の危険性を考慮して、以下の様な注意書きを書いています(同書P214)。
COLUMN: ファイル名の保存先には要注意

同書では、サンプル実行の簡単さを優先して、ドキュメントルートの配下にアップロードファイルを保存しています。しかし、これは不正なファイルをアップロードされたときに、そのまま実行されてしまう危険もはらんでいます。
一般的には、アップロードファイルは、ユーザーが直接アクセス出来ない--ドキュメントルートの外に保存するようにしてください。
これは実に正しい指摘ですが、それでは拡張子のホワイトリストチェックをしても、どうしてスクリプト実行されるかという疑問が生じます。その方法の一つが、Apacheの多重拡張子を悪用するという方法です。

多重拡張子

Apacheは元々の仕様として多重拡張子を扱えるようになっていて、その代表例はApache自身のマニュアルファイルに見ることができます。Apacheのマニュアルファイルには、以下のように各言語用のコンテンツが並んでいます。
index.html.en
index.html.fr
index.html.ja.utf8
最後の例だと、htmlに加えて、ja(言語=日本語)とutf8(文字エンコーディング=UTF-8)という具合に三種類の拡張子がついています。
これは、Apacheのディレクティブの中に拡張子によって挙動を変えるものが複数存在するためにおこる問題です。

ファイル名の途中にあるphp拡張子でスクリプトが起動する場合

このため、設定にもよりますが、phpinfo.php.png等ファイル名の末尾以外でも.php.が含まれていると、ApacheがこのファイルをPHPスクリプトとして判断してスクリプトを起動してしまう場合があります。具体的には、AddHandlerにより以下のようにPHPスクリプトを設定している場合が該当します。
AddHandler php5-script .php
そのような環境がどれくらいあるかは分かりませんが、メジャーなところではRHEL/CentOSが該当するので、影響は広範囲に渡りそうです。

「10日でおぼえるPHP入門教室 第4版」の環境は該当しない

それでは、「10日でおぼえるPHP入門教室 第4版」の環境(XAMPP)はどうかというと、PHPのApache設定は下記の通りで、この場合は上記の問題に該当しません。
#
# PHP-Module setup
#
LoadFile "C:/xampp/php/php5ts.dll"
LoadModule php5_module "C:/xampp/php/php5apache2_4.dll"

<FilesMatch "\.php$">
  SetHandler application/x-httpd-php
</FilesMatch>
従って、「書籍に書いてある通りに動かす分には脆弱性ではない」ということになります。冒頭で「微妙な問題」と書いた理由はこれです。

同書のスクリプトをCentOS上で動かすとスクリプト実行の脆弱性が発現する

しかし、同書でPHPを勉強した読者が、レンタルサーバー等を借りて自作のスクリプトを動かすと、脆弱性が悪用される危険性が生じます。
検証に必要な画像ファイルの作り方は、以前のエントリと同様で、JavaScriptの代わりにPHPスクリプトを書くだけです。そのようにして作成したファイルをアップロードしてブラウザからアクセスした様子を示します。実験にはCentOS6を用いました。


対策

対策には複数の種類があります。いずれか一つで対策になりますが、適宜複数を組み合わせることで安全性が高まります。推奨は (3)と(4)の組み合わせです。

(1)Apache側の設定を変える
XAMPPで用いている方法でPHPスクリプトを動かすようにする方法です。この場合は、ファイル名末尾が .php の場合のみPHPスクリプトとして動作するので、アプリケーション側の拡張子チェックをすり抜けることはありません。

(2)ファイルのアップロードディレクトリでPHPが動かないようにする
アップロードファイルがドキュメントルート配下に置かれる場合、該当のディレクトリでPHPが動かないようにする方法があります。httpd.confや.htaccessにて以下の設定を指定します。
RemoveHandler .php
この方法を推奨するというわけではありませんが、既存のアプリケーションで緊急対処が必要な場合には役立つでしょう。設定後、無害な test.php.png などのスクリプトでPHPとして動かないことを検査することと、検査後はそのファイルを必ず削除することを忘れないで下さい。また、php以外の拡張子を環境に応じて設定して下さい。

(3)アップロードファイルをドキュメントルート配下に置かない
この方法を強く推奨します。同書注意書きにもあるとおりですし、拙著『体系的に学ぶ 安全なWebアプリケーションの作り方 』でもこの方法を推奨しています。

(4)ファイル名を付け替える
そもそもブラウザから送信されたファイル名をそのまま使ってファイルを保存するという仕様はよくありません。ファイル名の衝突がありえるからです。ファイル名は他と衝突しないユニークなものを採番してつけるようにしましょう。これも拙著で推奨している方法です。これは、セキュリティ要件以前のアプリケーション要件として必要ですね。

まとめ

Apacheの多重拡張子の危険性と対策について説明しました。Apacheの多重拡張子は脆弱性ではなく仕様ですので、できればアプリケーション側で対処した方がよいと私は考えます。対策として説明した(3)と(4)は、Apache以外の環境でも安全性に寄与するため、強く推奨する方法です。
ファイルのアップロードはPHPから簡単にできてしまうので、PHP入門書でもしばしば取り上げられていますが、セキュリティの面からは危険な実装になっている場合が多いです。インターネットに公開するためには、セキュリティを強化する必要があります。
入門書だからそれでも仕方ないではないかという意見もあるでしょうが、私はそうは思いません。多くの読者は、それが危険な方法と知らないままに、その方法でインターネット上のサイトを作り続けるでしょう。最初から安全な方法を紹介するか、ファイルアップロード機能の紹介を諦めるか、どちらかです。つまり、ファイルのアップロードは、実は簡単ではないととらえるべきだと思います。

参考



【HASHコンサルティング広告】
この記事はHASHコンサルティング勤務中に書かれた。
HASHコンサルティング株式会社は、本物のセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。


2015年4月28日火曜日

みずほ銀行のトランザクション認証を試してみた

既に報道されているように、インターネットバンキングに対する不正送金事件が多発しています。
警察庁は2015年2月12日、2014年(平成26年)の1年間に発生した、インターネットバンキングでの不正送金事件の被害状況などに関するデータを発表した。

 不正送金事件の発生件数は1876件となり、前年の1315件から500件以上増加。被害額については約29億1000万円となり、前年の14億600万円から2倍超の増加となった。
2014年のネットバンキング不正送金は約29億円で法人被害が激増、警察庁発表 より引用
このような状況を受けて、フィッシング対策協議会では、インターネットバンキングの不正送金にあわないためのガイドラインとして以下の「鉄則」を公開しています。
  • 第一の鉄則:乱数表等(第二認証情報)の入力は慎重に!
  • 第二の鉄則:インターネット利用機器を最新の状態に保とう!
これらは確かに重要な施策ですが、現在多発していると言われるwebinject攻撃や、近い将来日本でも悪用されると予想されるMITB(Man in the Browser)攻撃に対しては十分とは言いにくいと考えます。

webinject攻撃や、MITB攻撃については以下を参照ください。
簡単に整理すると、これらはどちらもマルウェアがブラウザを操作するタイプの攻撃ですが、webinjectはログイン時等にニセの画面を表示して、乱数表の内容やワンタイムパスワード(OTP)を入力させるもの、MITBは利用者が振込操作をする際に、振込先や金額をマルウェアが変更するものです。

webinject攻撃は、利用者にOTPを入力するように促すため、利用者がサイトにログインする度に被害の可能性がある一方で、「このタイミングでOTPを入力するのはおかしい」と、利用者が気づく機会があります。一方MITBは利用者がOTPを入力するのをマルウェアがじっと待っています。このため、利用者が振込操作等をしない限り被害にあうことはありませんが、その代わり、利用者が不正送金に気づくことは不可能ということになります。

このような状況を受けて、みずほ銀行は、3月下旬より、トランザクション認証の提供を開始しました。
「トランザクション認証」機能とは、強固なセキュリティ対策の一つで、「登録先以外へのお振込」時に使用します。
「登録先以外へのお振込」時、お手元のワンタイムパスワードカードにお振込先の口座番号をご入力いただきワンタイムパスワードを発行します。このワンタイムパスワードとお振込先口座番号を紐づけ、お客さまが指定した振込先であるか確認します。
これにより、近年確認されている、悪意のある第三者がお振込先口座の情報を勝手に書き換え、お客さまが意図しない口座に振り込むという犯罪を、防止することができます。
みずほ銀行:ワンタイムパスワードカード より引用
従来みずほ銀行の口座は持っていなかったので、トランザクション認証を試すために口座開設して試してみましたので報告します。

トランザクション認証を使ってみる

自分のみずほ銀行の口座から、おなじく自分の三井住友銀行の口座に1万円振り込んでみます。まずは通常通り振込先と振込金額を指定すると以下の画面が表示されます。


この状態で、ワンタイムパスワードカードの ③ キーを押すと、以下のように入力が可能なモードに変わります。

ここで、「振込先の」口座番号7桁を入力します。以下の「9876543」は架空の口座番号です。


入力が終わったら右下の「OK」ボタンを押すと、以下のように 8桁のトークンが表示されます。


この 8桁トークンをウェブ画面の「ワンタイムパスワード」欄に入力して、「振込実行」ボタンをクリックしすると、振込が実行されます。
利用者が入力したトークンは、カード上で入力した口座番号にのみ有効なので、仮にMITB攻撃でトークンが窃取されたとしても、(たまたま口座番号が一致しない限り)攻撃者の口座に不正送金されることはありません。
この後、振込先口座を登録することができ、一端登録した口座は、次回からは「信頼された口座」として、トランザクション認証なしに振込が可能になります。この口座登録は、(1)銀行窓口で登録、(2)振込操作の後に登録、の二種類でしかできないので、攻撃者が自分の口座をまず登録するという手口はできないと思われます。

残る脅威

webinject攻撃等で、マルウェアが画面を操作して、利用者にトランザクション認証カードの操作を指示し、「攻撃者の口座番号」をカードに入力するように誘導する攻撃の可能性がある、と高木浩光氏が指摘しています。
高木氏の指摘は、三井住友銀行やゆうちょ銀行のように、「トランザクション認証機能つきカードを配ったが、現在は単純なワンタイムパスワードとして用いていて、将来トランザクション認証方式に変更する」場合の運用を危惧したものですが、みずほ銀行の場合は、最初からトランザクション認証用としてカードを配っているので、被害にあう可能性は相対的に低くなるでしょう。
とはいえ、やはり騙される人はいると予想しますが、カードに同封された説明書には、以下のように注意がありました。素晴らしいですね。


ここまでしても騙される人はいるでしょうが、まずは、みずほ銀行の努力と気配りを賞賛したいと思います。

まとめ・感想

みずほ銀行が「日本で最初に導入」したトランザクション認証を試用してみました。
不正送金の手口はどんどん進歩していて、利用者側の注意だけでは被害を防ぐことは難しい状況になっています。このため、本格的なMITB防止機能としてトランザクション認証機能を提供したみずほ銀行の英断は素晴らしいと思います。
とはいえ、前述したように、それでもダマされる手口は残る可能性はあることと、そもそも操作が難しくて普及が難しそうだと感じました。
元々はトランザクション署名の試験用に口座を開設したみずほ銀行の口座ですが、月に4回までは振込料金が無料ということを知り、本来の銀行口座としても有効活用していこうと思います:)

追記(17:15)

何気なくパスワードカードの裏を見ると下図のようになっていました。


以下の文言が気になりました。
※登録先以外へのお振込み時には振り込み画面上の【手順】に従ってお手続きください。
これはいただけない。振込画面はマルウェアによって改ざんされている可能性があるからこそのトランザクション認証なので、画面通りに操作すると不正送金の被害にあう可能性があります。この文言は以下のようにすべきところです。
※登録先以外へのお振込み時には、カード同梱の説明書の【手順】に従ってお手続きください。
セキュリティって難しいですね。


【HASHコンサルティング広告】
この記事はHASHコンサルティング勤務中に書かれた。
HASHコンサルティング株式会社は、本物のセキュリティエンジニアを募集しています。
興味のある方は、twitterfacebookのメッセージ、あるいは問い合わせページからお問い合わせください。

フォロワー