安全なAPI過信症候群の処方箋 – execv/SQLite3編

(Last Updated On: 2018年8月13日)

またプリペアードクエリなど、安全とされるAPI万能と考えている方に会ったのでエントリを書きました。広く病気として治療すべき、と思いエントリを書きます。安全なAPI過信症候群と名付けました。

安全なAPI過信症候群(同類にプリペアードクエリ過信症候群など):「安全」とされるAPIを使えば安全と、盲目的に信用し考慮すべきリスクを考えない症候群。ITエンジニアが発症し最も重要なセキュリティ対策である入力バリデーションを「必要ない、できない、セキュリティ対策ではない」エスケープは「必要ない、有害である」とする場合、かなり重度の場合が多い。

このブログでは既に何度もプリペアードクエリクエリは不完全である、と指摘しています。プリペアードクエリを使っていれば安全と盲目的に信じている方向けの基礎知識を紹介します。コマンド実行APIのexecvと最も多く利用されているRDBMS、SQLite3が題材です。

現実を知れば安全とされるAPIを盲信することも危険だと解り、安全なAPI過信症候群を完治できると思います。

追記:普通のRDBMS編も作りました。SQLiteの仕様でデータ型がどうなっているか簡単に説明しました。

安全と言われているAPIでも完全に安全なインジェクション対策ではない

動的・静的を問わずプリペアードクエリは識別子の分離ができないプリペア文への不正な命令の埋め込みが禁止できない、という問題がありセキュリティ対策としては不完全なセキュアAPIです。

プリペアード文がexecv()のようにコマンド(命令)と引数(データ)を完全に分離し、コマンドへの埋め込みを防止できるならかなり安全です。”かなり安全”としているのは、execv()などを使っていても、コマンド指定文字列にヌル文字インジェクションを行い不正な命令を実行できるからです。

execv()とはコマンドを実行するC APIです。execvにはexecve、execvepなど色々あります。コマンド文字列と引数配列を引数として持ち、命令とデータが分離されているので”かなり安全”にコマンドを実行できます。

しかし、何も考えずに使っても大丈夫でしょうか?次のPHPコードを見てみましょう。pcntl_execはexecv/execve APIを利用しています。

<?php
$cmd = "/bin/ls\0/usr/local/ping";
$args = array(); //ユーザー入力
$env = array('PATH'=>'/usr/local');
// 実行できるコマンドを制限しているつもり
if (!mb_ereg('(ping|whois)¥z', $cmd)) {
  die('残念!実行できません');
}
// ヌル文字インジェクションに脆弱なので/bin/lsが実行される。
pcntl_exec($cmd, $args, $env);

“/bin/ls”でなく”/bin/sh”を設定して引数も複数指定できるなら、攻撃者はやりたい放題です。execv()系のAPIを使っていれば安全!と思い込んでいた方、ご注意ください。

注)C/C++言語では文字列の終端はヌル文字です。

「安全」と言われているAPIでも「盲信」するとセキュリティ問題を生む原因になる良い例の一つです。安全に利用するには仕様を理解している必要があります。

プリペアードクエリを使っていればデータは分離されるから絶対に安全でしょうか?

次のコードは改定予定のPHPポケットリファレンスに記載しているSQLite3用のコードです。今のポケリにも同じようなコードを書いていたと思います。

<?php
@unlink('/tmp/sqlite3.db');
$db = new SQLite3('/tmp/sqlite3.db');
// INTEGER PRIMARY KEY AUTOINCREMENTがlastInsertRowIDのIDとなる
$db->exec('
 CREATE TABLE test (
  id INTEGER PRIMARY KEY AUTOINCREMENT, 
  username TEXT,
  groupid INTEGER)
');

// 行を挿入
$stmt = $db->prepare('INSERT INTO test (username, groupid) VALUES (:name, :group)');

// 変数をバインド
$param = 'bar';
$stmt->bindParam(':name', $param);
// 直接値を書く場合
$stmt->bindValue(':group', 987654321);
$stmt->execute();

// 変数を変更し、整数を大きくして実行
$param = 'foo';
$stmt->bindParam(':name', $param);
$stmt->bindValue(':group', 9876543210);
$stmt->execute();
// 整数型の変数」に文字列を設定
$stmt->bindValue(':group', '日本語テキスト');
$stmt->execute();
// 型ヒントを利用して整数型の変数に文字列を設定
$stmt->bindValue(':group', '日本語テキスト', SQLITE3_INTEGER);
$stmt->execute();

// 挿入した行を取得
$result = $db->query('SELECT * FROM test;');
while($row = $result->fetchArray(SQLITE3_ASSOC)) {
  var_dump($row);
}

そうとは限らないことは、このコードで明らかです。SQLite2は全てのデータをテキストとして保存していました。SQLite3になって内部的にはデータ型を持つようになりましたが、SQLite3のコラムは「動的」です。動的とはスキーマのデータ型に関係なく、どのようなデータでも保存できるということです。

// 整数型の変数」に文字列を設定
$stmt->bindValue(':group', '日本語テキスト');
$stmt->execute();

:groupのコラムは整数型

groupid INTEGER

と定義されていますが、上記のコードのようにSQLite3では整数型のコラムに文字列も保存できるのです。つまり、「このコラムは整数だから、戻り値も整数」と思って手抜きをすると、JavaScriptインジェクションなどのインジェクション攻撃に脆弱になります。これは有名(?)な仕様なので、ほとんどの開発者は知っていると思います。このような仕様なのでSQLite3ではプリペアードクエリを使っていても「整数型なので安全」と盲信するとNGであることをこれ以上説明する必要は無いと思います。

これはSQLite3の仕様がおかしい、悪い、と言っても仕様です。そしてSQLite3は恐らく世界で最も多く利用されているRDBMSです。

安全なAPI過信症候群の処方箋

安全なAPI過信症候群の処方箋

  1. 「出力先の仕様を知る」 –  安全なAPIを使っているから、だけではダメです。SQLiteの仕様をよく知らずに、一般的なデータベースなら整数コラムに整数以外は入らない、と思い込んでいるとNGです。APIの仕様と出力先の仕様、両方を正しく理解する必要があります。
  2. 「入力バリデーションは必須」 –  SQLiteの仕様をよく知らずに作ってしまった、としてもアプリの入力バリデーションを行なっていれば、整数コラムに文字列が入ることはない。この例に限らず、入力バリデーションは多くの脆弱性に対して有効です。
  3. エスケープの知識は必須」- エスケープは悪ではありません。テキストインターフェースの基礎的な機能です。テキストインターフェースを作る場合、エスケープを正しく実装しないとセキュリティ上の問題になります。その良い例はシェルの仕様、XPath1.0の仕様です。

「入力バリデーションは未知の脆弱性に対応できる」とWebアプリセキュリティ対策入門で解説していますが、先ほどのexecv()やSQLiteのようなケースを言っています。

以上で安全なコードを書くために必要なSQLiteの知識を十分もった、といえるでしょうか?答えはNOです。

// 型ヒントを利用して整数型の変数に文字列を設定
$stmt->bindValue(':group', '日本語テキスト', SQLITE3_INTEGER);
$stmt->execute();

このコードを実行した結果、groupコラムにはどのような値が入るでしょうか?

整数の0が保存されます。gruop ID=0の場合に特殊な意味、普通は管理者など、を持つアプリケーションは多いでしょう。

 

データ型指定は弱いバリデーションに過ぎない

データ型を指定した方が安全!確かに、多くの場合は安全になります。しかし、0が入ってしまうと

if ($group_id === 0) {
  // 管理者として実行するコード
}

と、データ型を意識した比較を行っても無意味です。元々整数なのでPHPよりデータ型に厳しい言語でも問題を防げません。 (整数の0になっているのはPHPの仕様ですが、同類の動作をするSQLiteインターフェースもあると思います。他の実装で調べた方、コメントで教えていただけると助かります。)

データ型を指定すると丸め誤差やオーバーフローが発生することもあります。データ型を指定することは常に安全ではないことを知っておく必要があります。

参考:

 

まとめ

出だしはあえて狙って書いてみました。「安全なAPI(プリペアードクエリやexecv)を使っていれば安全」には病気として対処すべき時期ではないでしょうか‥ 現実にリスクがあるにも関わらず、それを認めず・無視し、安直に安全なAPIを使えば良い、ではいつまで経っても安全になりません。私は開発者はセキュリティ問題に自分で対処できると信じていますが、思考停止していては対処しようがありません。

ここに書いたことを既にご存知であった方も多いと思います。そうでない方は安全なAPIを使っているから絶対に安全!と盲信することが危険であることが良く理解できたと思います。

安全なAPIを使えば安全!

今後はこういう教育の仕方はやめにしませんか?安全なAPI過信症候群の方、完治しましょう!

Node.js、Python、Ruby、Java、.NET、ObjectiveCなどなど、SQLite3インターフェースの実装を教えていただけると有り難いです!(昔にざっとは見ていますが、現在はどうなっているのは見ていません)

最後に上記のPHPコードを実行した結果(OS X 10.10 PHP 5.6)を掲載します。

array(3) {
  'id' =>
  int(1)
  'username' =>
  string(3) "bar"
  'groupid' =>
  int(987654321)
}
array(3) {
  'id' =>
  int(2)
  'username' =>
  string(3) "foo"
  'groupid' =>
  string(10) "9876543210"
}
array(3) {
  'id' =>
  int(3)
  'username' =>
  string(3) "foo"
  'groupid' =>
  string(21) "日本語テキスト"
}
array(3) {
  'id' =>
  int(4)
  'username' =>
  string(3) "foo"
  'groupid' =>
  int(0)
}

 

投稿者: yohgaki