(Last Updated On: 2018年11月5日)

PostgreSQL 11がリリースされました。このリリースでto_number()、to_char()、to_date()、to_timestamp()関数の仕様が変更されました。これらは名前の通り入力を変換する関数です。その際に

  • サニタイズ – ダメな形式のデータを使える/安全なデータ形式に変換する

を行います。保存されるデータ形式は、保存可能な形に変換されます。しかし、これは十分ではないです。遅すぎるサニタイズがダメな例として紹介します。

※ エスケープやプリペアードクエリもサニタイズ(無害化)の一種です。

PostgreSQLのto_number()仕様

to_number()関数は高機能で色々な変換が可能です。全てを紹介しきれないのでカンマで千桁を区切るケースだけ紹介します。

to_number()の仕様

to_number(text, text)numericconvert string to numericto_number('12,454.8-', '99G999D9S')

1番目の引数が入力文字列で2番目の文字列フォーマット指定用の文字列です。

千桁カンマ区切りの入力を数値に変換するには以下のようにします。

to_number('1,234,567,890', '9,999,999,999')

実行結果:

yohgaki@127 ~=# SELECT to_number('1,234,567,8901234', '9,999,999,999');
 to_number  
------------
 1234567890

yohgaki@127 ~=# SELECT to_number('1,234,567', '9,999,999,999');
 to_number 
-----------
   1234567

カンマが削除され、numeric型の値に変換されます。

無効な入力に対するサニタイズ

入力文字列にnumeric型として無効な文字が含まれている場合はサニタイズします。

PostgreSQL 10までのサニタイズ:

yohgaki@127 ~=# SELECT to_number('a1,234,567', '9,999,999,999');
 to_number 
-----------
      2356

yohgaki@127 ~=# SELECT to_number('1234567890', '9,999,999,999');
 to_number 
-----------
   1345789

仕様を知らないと、何が何だか訳けが分からない結果になっています。それでも仕様的には正しい”サニタイズ”です。

PostgreSQL 11ではnumeric型で利用する “,”、”+”、”-“、”.”以外の文字はフォーマットに合わなくても”サニタイズ”しない仕様に変わりました。 

PostgreSQL 11のサニタイズ:

yohgaki@127 ~=# SELECT to_number('a1,234,567', '9,999,999,999');
 to_number 
-----------
   1234567

yohgaki@127 ~=# SELECT to_number('1234567890', '9,999,999,999');
 to_number  
------------
 1234567890

PostgreSQL 11では取り敢えず納得可能な変換になりました。

しかし、PostgreSQL 11では以前よりマシなサニタイズを行いますが、無効な入力をサニタイズでは”どう頑張ってもダメ”です。

サニタイズでは正しく動作させることができない

PostgreSQL 11より以前ではto_number()関数は出鱈目な入力データに対して、本当に出鱈目な結果を返していました。PostgreSQL 11では随分マシになっています。マシになったとはいえ、出鱈目なデータをサニタイズしても、出鱈目なデータであることは変わりません。

指数表記の値は正しく変換されない:

yohgaki@127 ~=# SELECT to_number('1,234,567e10', '9,999,999,999');
 to_number 
-----------
 123456710

大きすぎる値は正しく変換されない:

yohgaki@127 ~=# SELECT to_number('1,234,567,890,123', '9,999,999,999');
 to_number  
------------
 1234567890

無効文字と長過る部分は無視される:

yohgaki@127 ~=# SELECT to_number('1234WERT567890', '9,999,999,999');
 to_number 
-----------
   1234567

サニタイズは”ダメな物を無理矢理にでも使える形”に綺麗にします。無理に変換してダメな物でも使えるようにしてしまします。上記のような変換&保存が”正しい動作である!”という人は居ないでしょう。

遅すぎるサニタイズはNG

入力データバリデーションを実施する際には次の鉄則があります。

  • できる限り早くバリデーションする
  • ただし、全ての正規化が完了してからバリデーションする(=以後、基本的に変換は行わない)

”正規化”とはデータを処理する前に必要な変換作業(UnicodeのNFD -> NFC変換など)を言います。サニタイズも”正規化”の一種です。

(そもそも、そのような脆弱な仕様にすべきではありませんが)無効な形式の数値を受け入れる仕様の場合、データベース保存時にサニタイズするのは遅過ぎです。

そもそも’1234WERT567890’といった意味のない文字列を含むデータを”数値”として扱うプログラムは正しい動作している、とは言えません。(そういう仕様もありえますが、’1234WERT567890’はどう見ても10進数形式ではありません)

PostgreSQLとしては”仕様”としてサニタイズしてしまう動作は致命的とまでは言えません。しかし、アプリケーションとしては”無意味な無効データ”を”取り敢えず保存できる形にして保存してしまう”動作は致命的と言えます。

どうすれば良いのか?

PostgreSQLとしては無効なデータを取り敢えず保存できてしまう”仕様”である、と言い張ることも可能です。しかし、サニタイズではなくSQLエラーにしてしまう方が一般には好ましいです。(今のところPostgreSQLにはSQLエラーにする設定などはない)

しかし、仮にSQLエラーになってもアプリケーションとしてはSQLエラーに頼るのはNGです。無効な数値データを処理し続け、最終的にデータベースにまで出力してしまう、といった動作はセキュアなプログラミングの原則に反します

基本、全ての遅すぎるサニタイズ(無害化)は脆弱でダメ

  • できる限り早くバリデーションする
  • ただし、全ての正規化が完了してからバリデーションする(=以後、基本的に変換は行わない)
  • フェイルファースト – 失敗するものはできる限り早く失敗させる

これらはセキュアコーディング/セキュアプログラミングの基本的な原則です。

勘違いしてはいけないのは、

  • 全ての出力データを無害化する

セキュアコーディング/セキュアプログラミングの基本的な原則です。

出力対策の役割には”必須のセキュリティ処理”と”フェイルセーフ対策”があります。フェイルセーフ対策も必要ですが、フェイルセーフ対策に頼る脆弱なソフトウェアは基本全てダメなセキュリティ構造と言えます。

参考: SQLクエリをサニタイズするだけ、では穴だらけのセキュリティ

参考: ライブラリにセキュリティ対策の責任を負わせることは不可能

参考: コマンドと引数を分離する、は全く不十分な対策

投稿者: yohgaki