プライバシーポリシー

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のメッセージ、あるいは問い合わせページからお問い合わせください。