SQLite3の全てのカラムがテキスト型である問題に対する誤解

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

以前にSQLite3のデータ型は基本的には全てテキスト(例外は整数型プライマリーキー ※)である、という解説をしました。

どうもこの問題は強い型を持っている言語には影響がないとの誤解があるようなので解説します。ついでに明らかだとは思いますが、他のリスクも紹介します。

※ 正確にはBLOB型も例外になります。テキストではないデータも保存できます。SQLiteのサイトでは整数型プライマリーキーは例外と記述されていましたが、手元のSQLiteで試すと文字列も保存できてしまいました。

参考:SQLite3のカラム仕様を理解している必要があります。

SQLiteデータ型の仕様とセキュリティ問題

SQLiteのデータ型変換問題

SQLiteは基本的にカラムのデータ型に関わらず”テキスト”として保存します。弱い型を持つ言語(例えばPHP)の場合、データベースへの保存する前のバリデーションが甘い場合、整数型などにテキストが保存されます。プリペアードクエリ、エスケープのどちらを使っているのか、は関係ありません。

例えば、INTと定義されたint_data_typeのカラムは、文字列でも保存可能です。(Type Affinity – 型親和性という仕様です)

INSERT INTO tbl (int_data_type) VALUES ('文字列');

一般的なDBではレコード保存時にエラーとなり保存できない”文字列”が保存できてしまい、整数キャストなどで

$id = (int) "文字列";

とすると$idには0が入ります。0は特別な意味を持つ事が多い値で、問題の原因になります。

0に変換される問題以外にも問題となる場合があります。

現在は全ての変数はHTMLエスケープしてから出力するのが当たり前ですが、SQLiteの整数型などのカラムはHTMLコンテクストにそのまま出力しても安全、と思っているとインジェクション攻撃に脆弱になる可能性があります。

数値だからHTML/JavaScript/NULL文字/改行インジェクションなどはできないはず、と思っていると問題になる可能性があります。”文字列”として保存されて、”文字列”として返されるので、ありとあらゆる間接インジェクション攻撃の可能性があります。

型が強い言語で、整数型の変数に保存している場合でも「意図せず整数の0になる」「エラーによるサービス不能攻撃に脆弱になる可能性」という問題に限定されているだけです。サービス不能攻撃も重大なセキュリティ問題の1つです。

整数型などに文字列が入ってしまうとエラーになるようなアプリケーションでは、インジェクションリスク以外にサービス不能攻撃に非常に脆弱となる可能性があります。ほとんどプログラムはRDBMSから戻って来た値が「データ型」から異なることを予期したコードになっていないからです。

参考:

出力対策の3つの役割 – フェイルセーフ頼みはNG

RDBMSが戻すデータはテキスト

RDBMSが戻すデータは基本的に”テキスト”です。(DBMS/APIによって異る場合もある)

例えば、PostgreSQL/MySQLのC APIでは以下の様にカラムのデータが”文字列型”として返されていることが分かります。

PostgreSQL

char *PQgetvalue(const PGresult *res,
                 int row_number,
                 int column_number);

http://www.postgresql.org/docs/9.4/static/libpq-exec.html#LIBPQ-EXEC-SELECT-INFO

MySQL – APIシグニチャがないのでサンプルプログラムを掲載。rowは”文字列型”配列。

MYSQL_ROW row;
unsigned int num_fields;
unsigned int i;

num_fields = mysql_num_fields(result);
while ((row = mysql_fetch_row(result)))
{
   unsigned long *lengths;
   lengths = mysql_fetch_lengths(result);
   for(i = 0; i < num_fields; i++)
   {
       printf("[%.*s] ", (int) lengths[i],
              row[i] ? row[i] : "NULL");
   }
   printf("\n");
}

http://dev.mysql.com/doc/refman/5.7/en/mysql-fetch-row.html

データベースが表現できる”データ”はプログラミング言語のデータ型で表現できる範囲に収まるとは限りません。データが別の値に変わってしまうことを防ぐため、データベースは基本的にテキストとしてデータを返します。

プログラムの処理が正しいか?も重要

“テキスト”として返って来たデータをどのように扱うかはプログラム次第です。例えば、PHPのPostgreSQLモジュール、MySQLiモジュールはデータベースカラムの値を”テキスト”として返します。

例えば、静的な型を持つC言語の文字列変換関数atoi()、atol()などの場合、

atoi() 関数は、nptr によって指示される文字列のはじめの部分を int 型整数に変換する。 この振る舞いは、
atoi() 関数がエラーを見つけない点以外は、

strtol(nptr, NULL, 10);

と同じである。

となっています。言語が静的な型を持っているのかいないのか、はSQLiteのカラムが基本的に”テキスト”である問題とは関係ありません。

プログラムの処理が正しいか?も問題になります。特に問題になりがちなデータはNumeric型のデータです。Numeric型はプログラミング言語のネイティブ整数型が処理できる範囲を超えることが可能です。

SQLite3データベースアクセスライブラリの利用者はライブラリがエラー(文字列型データを非文字列型データのカラムへ入力/出力した場合のエラー)を検出できるかどうか?が問題になります。通常の強い型付けのデータベースのみを想定しているライブラリの場合、全くエラーとならない場合もあるでしょう。

エラーが発生する場合でもサービス不能攻撃に脆弱性になるリスクもあるので、結局はライブラリなどに頼らず「データベースへの出力データが正しい」ことを保証する方が良いです。(=入力データをバリデーションする方が良い)

参考:

入力値の種類は3種類しかない

バリデーションには3種類のバリデーションがある 〜 セキュアなアプリケーションの構造 〜

まとめ

データベースへ正しい値を渡す責任はアプリケーションにあります。SQLiteも利用する場合、カラムへ保存するデータ、保存されてデータに追加の注意が必要で、これは言語のデータ型仕様がどうなっているのか?はあまり関係がありません(リスクがゼロではない)。型が弱い言語の場合、様々なインジェクション攻撃のリスク、サービス不能攻撃のリスクが増加するだけで、データ型の強い/弱いに関わらず同じ注意が必要です。

入力を受け付けた時にバリデーション(境界防御)、データベースに保存(出力)する前にバリデーション(多層防御)を組み合わて対応すると良いでしょう。

ライブラリを利用していて、そのライブラリがエラーなく処理(数値でない文字列を数値に変換)してしまう場合は特に注意が必要です。動作がどうなっているかよく分からない場合は、整数型などのカラムに”文字列”を保存し、取得した時どうなるか確認すると良いでしょう。

セキュリティを維持するには入力/出力を確実に制御することが重要です。確実に制御するには入力先/出力先の仕様を良く理解します。Webアプリの場合、ほとんどの入力/出力がテキストなので正確なテキスト処理が重要です。

何だかややこしい話だな、と感じたかも知れません。未検証入力は脆弱性の1つで、問題をややこしくする大きな原因だからです。

投稿者: yohgaki