SQLインジェクション対策はおすみですか?
開発開始時点からのコンサルティングから、公開済みWebサイトの脆弱性検査、
脆弱性発見後の適切な対策まで
トップ 最新 追記
過去の日記

2008-06-01 そろそろSQLエスケープに関して一言いっとくか

SQLのエスケープ再考

本稿ではSQLインジェクション対策として、SQLのエスケープ処理の方法について検討する。

最近SQLインジェクション攻撃が猛威を振るっていることもあり、SQLインジェクションに対する解説記事が増えてきたようだが、対策方法については十分に書かれていないように感じる。非常に稀なケースの対応が不十分だと言っているのではない。ごく基本的なことが十分書かれていないと思うのだ。

SQLインジェクション対策には二通りある。バインド機構を使うものと、SQLのエスケープによるものだ。このうち、SQLのエスケープについて、十分に書かれているテキストが見当たらないのだ。このため、自分で書いてみようと思う。

IPAの「安全なウェブサイトの作り方改訂第三版」ではSQLのエスケープについて以下のように説明されている。

1)-2 バインド機構を利用できない場合は、SQL 文を構成する全ての変数に対しエスケープ処理を行う

解説  これは、根本的解決 1) のバインド機構を利用した実装ができない場合に実施すべき実装です。
 利用者から入力されるパラメータや、データベースに格納された情報などに限らず、SQL 文を構成する全ての変数や演算結果に対し、エスケープ処理を行ってください。エスケープ処理の対象は、SQL文にとって特別な意味を持つ記号文字(たとえば、「'」→「''」、「\」→「\\」 など)です。
 なお、SQL 文にとって特別な意味を持つ記号文字は、データベースエンジンによって異なるため、利用しているデータベースエンジンに応じて対策をしてください。データベースエンジンによっては、専用のエスケープ処理を行うAPIを提供しているものがあります(たとえば、Perl ならDBIモジュールのquote()など)ので、それを利用することをお勧めします。

引用した部分は、一般的な内容を網羅しているものの不満もある。短い文章の中で「など」が3回も現れることに象徴されるように、あいまい性の残る文章となっている。これはデータベースの製品依存のところでやむを得ないというのはよく理解できるのだが、結果として、読者に「データベースエンジンのマニュアルを読め」と言っているの等しい。それでいて、読者には、マニュアルのどこをどう調べたらよいかまでは示していない。「エスケープ処理の対象は、SQL文にとって特別な意味を持つ記号文字」と書かれているが、この説明だと、「;」や「=」、空白までエスケープしなければならないと誤解する読者が出てくるかもしれない。実際には、これらの文字をエスケープする必要はないし、SQLの標準規格には、そもそもこれらの文字をエスケープする手段が用意されていない。エスケープの必要がないので手段も用意されていないのだ。

そこで、「安全なウェプサイトの作り方」を補完するような形で、もう少しSQLエスケープについて書き足してみたい。

SQLインジェクションのおさらい

SQLインジェクション攻撃では、SQLに渡すパラメータ部分にSQL断片を挿入し、SQLの意味を書き換えることによって行われる。 バインド機構を使わずに自前でSQLを組み立てる場合、SQLに対するパラメータはSQLのリテラル(定数)の形で渡される。

リテラルには複数の型があるが、通常問題になるのは数値と文字列である。

数値リテラルの場合:

SELECT * FROM XXXXX WHERE NNUM=●●●      -- ●●●は数値、例えば 123

文字列リテラルの場合:

SELECT * FROM XXXXX WHERE EID='■■■'      -- ■■■は文字列、例えば S853

なぜリテラルを構成する文字列(●●●や■■■)を操作することでSQLの構文まで変わるのか。それは特殊記号などでリテラルを終端させ、その後にSQL断片を埋め込むからだ。

数値リテラルを終端させるには、数値以外の文字を加えればよい。

例: 123OR TRUE         -- 数値リテラル123の後に、OR TRUE が続く。123 OR TRUEと同じ

文字列リテラルを終端させるには、単一引用符「'」を加えればよい

例: A'OR'A'='A

この例を先のSQLに適用すると、

SELECT * FROM XXXXX WHERE EID='A'OR'A'='A'

となる。つまり、WHERE句は常に真となる。

SQLインジェクション対策の基本的な考え方

すなわち、SQLインジェクション対策の方針としては、リテラルを勝手に終端させないようにすることが必要なのだ。そして、この処理は数値リテラルと文字列リテラルとでは、方法が異なる。

数値リテラルの場合は、数値以外の文字が出てきた時点でリテラルの終端となるので、数値としての妥当性検証を行うことになる(数値項目に対するSQLインジェクション対策のまとめ参照) 。

一方、文字列リテラルの場合は、文字列リテラルのエスケープを行えばよい。SQL92などSQLの標準規格で規定しているのは、単一引用符「'」のエスケープであるが、データベースの種類によっては、円記号(バックスラッシュ)「\」のエスケープも必要となる。

商用データベースの場合

商用データベースの場合は、SQL標準に従い、単一引用符「'」を「''」と重ねる処理を行う。OracleSQL ServerIBM DB2についてはリファレンスと動作の両方で確認した。

オープンソース・データベースの場合

オープンソースのデータベースとして広く普及しているMySQLの場合、文字列リテラル中にC言語風の「\」を使ったエスケープシーケンスが記述できる。従って、文字列リテラル中に「\」自体を記述する際には、「\\」とする必要がある。一方、単一引用符「'」は「''」としてもよいし、「\'」としてもよい。その他、二重引用符「"」が利用できるなど自由度が高い。

オープンソース・データベースのもう一方の雄PostgreSQLの場合は事情が少し複雑となるが、デフォルトではMySQLと同じで、単一引用符と円記号(バックスラッシュ)の両方をエスケープする必要がある。しかし、設定パラメータstandard_conforming_stringsがonの場合(デフォルトはoff)は、Oracleなどと同じ挙動となる(現実にはもう少し複雑だが稿をあらためて説明したい)。

まとめると以下のようになる。

データベース 元の文字 エスケープ後
Oracle
MS SQL
IBM DB2
' ''
MySQL
PostgreSQL
' '' または \'
\ \\

なお、念のため補足すると、エスケープ処理はセキュリティ対策のために行うものでは元々なく、与えられたパラメータに対して正しく処理を行うために必要な処置である。入力に「'」や「\」が使えないと不便で仕方がないし、現実的に不具合が生じるだろう。

その他のデータベース製品の場合はどうしたらよいか

今回説明した内容にもっとも近い記述があるドキュメントとしては、佐名木智貴氏の近著「セキュアWebプログラミングTips集(ソフト・リサーチ・センター)」がある。同書では、SQLエスケープの基本として

「'」は、「''」(シングル・クォート2個)にエスケープ処理することで、SQLインジェクションから防御することができる(同書P210)

とした上で、

mySQLとPostgreSQLの場合のSQLインジェクション対策として、入力データをSQL文の文字列リテラルとして使う場合、「'(シングル・クォート)」と「\」をSQLエスケープすること(同書P213)

と指摘している。

非常に丁寧な仕事ぶりで好感を持った。ただ、同じページの以下はいただけない

筆者の知らないデータベース・ソフトウェアでは、SQLが拡張され、それ以外のメタキャラクタもあるかも知れない。ぜひ読者諸氏には今一度、自分の使っているデータベース・ソフトウェアのSQLリファレンスを通読することを推奨する(同書P213)。

「SQLリファレンスを通読」とは・・・無茶言うなよと思う。

実際には通読する必要はなく、「リテラル」、「定数」、「文字列」などのキーワードを手掛かりに、文字列リテラルの項を探すとよい。本稿で引用したリファレンスもこのようにして探したものである。

Oracleの場合を例にマニュアルの見方を説明しよう。題材として、Oracle10gのオンラインマニュアルを利用する。

まず、目次から「リテラル」を探すと、「2 Oracle SQLの基本要素」に「リテラル」や「テキスト・リテラル」という項が見つかる。「テキスト・リテラル」の項を読むと、

  • cは、データベース・キャラクタ・セットの任意の要素です。リテラル内の一重引用符(')の前には、エスケープ文字を付ける必要があります。リテラル内で一重引用符を表すには、一重引用符を2つ使用します
  • ''は、テキスト・リテラルの始まりと終わりを示す2つの一重引用符です。

このように、エスケープの必要な文字は一重引用符「'」であること、エスケープの仕方は一重引用符を2つ使用することであることがわかる。他のデータベース・ソフトウェアでも、同様に探すことができるだろう。

本稿を参考に正しいSQLインジェクション対策を実施していただきたい。


続く(徳丸浩の日記 - SQLインジェクション対策 - SQLエスケープにおける「\」の取り扱い)

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

佐名木 [mySQL での「\」のエスケープについて知ったのは、(私にとっては無知の知の領域にある)mySQL のリファレンス..]

本日のリンク元 | 9 | TrackBacks(3)

2008-06-02 SQLインジェクション対策

SQLエスケープにおける「\」の取り扱い

昨日のエントリ(徳丸浩の日記 - そろそろSQLエスケープに関して一言いっとくか - SQLのエスケープ再考)は思いがけず多くの方に読んでいただいた。ありがとうございます。その中で高木浩光氏からブクマコメントを頂戴した

\がescape用文字のDBで\のescapeが必須になる理由が明確に書かれてない。\'が与えられたとき'だけescapeすると…。自作escapeは危うい。「安全な…作り方」3版で追加の「3.失敗例」ではDBで用意されたescape機能しか推奨していない

このうち、まず「\」のエスケープが必須となる(MySQLやPostgreSQLで)理由を説明しよう。

「\」をエスケープしないと処理がおかしくなる

MySQLにおいて、文字列「\n」は改行を意味する。その他、\に続く文字によって、様々な制御文字などを表現できるようになっている。

このため、ユーザがたまたま入力欄などから「\n」と入力した際に、エスケープしないままだと、ユーザの意図に反して「\n」が改行に化けることになる。また、エスケープシーケンスとして定義されていない場合、たとえば「\x」は単に「x」を表すと規定されているので、「tokumaru.org大安売り¥100-」が「tokumaru.org大安売り100-」となり、「¥」が欠落してしまう。これはユーザの意図ではない。

このため、「\」を「\\」エスケープすることにより、上記のような文字化けを防ぐ必要がある。これが、そもそも「\」のエスケープが必須となる理由で、セキュリティ上の要求がなくても、必要な処理である。

「\」のエスケープもれによるSQLインジェクション

上記に加えて、「\」のエスケープが必要な状況でそれがもれている場合、SQLインジェクション脆弱性の原因となる。高木氏が指摘しておられるように、「\'」という入力に対して「'」のみエスケープすると、「\''」という文字列になる。前半の「\'」で「'」を表すので、末尾の「'」がエスケープされないで残ってしまう。つまり、文字列リテラルを終端できる。前回指摘したように、これによりSQL断片を埋め込むことが可能となる。この例だと分かりにくいので、もう少し現実的な攻撃パターンで説明しよう。

SELECT * FROM XXX WHERE NN='$id'

$id として \'or 1=1# が入力されると

\'or 1=1#
  ↓ エスケープ
\''or 1=1#

元のSQLに適用すると、

SELECT * FROM XXX WHERE NN='\'' or 1=1#'

すなわち、SQLの構文が改変された

上記でデータベースとしてはMySQLを想定している。「#」はMySQLでコメントを表すので、行末の「#'」は無視される。

Shift_JISでの問題

ここまでならまだよい。データベースの種類によっては「\」のエスケープを忘れないようにしようで済む。ところが、文字「\」を表す文字コード0x5cがShift_JISの2バイト目にも現れうることから話がややこしくなった(一方、「'」を表す0x27の方はShift_JISの二バイト目に現れない)。0x5cを二バイト目に含む文字は多数あるが、例として以下を紹介する。

ソ(835c)
能(945c)
表(955c)
予(975c)

上記のように出現頻度の高い文字が含まれている。言語処理系やデータベースエンジン、APIなどに日本語処理の不完全な部分があると、SQLインジェクションの可能性が出てくる。

データベースエンジンの日本語処理が不完全な場合

この場合は、「表'」のような組み合わせによりSQLインジェクションができる可能性がある。

'
0x95 0x5c 0x27
 ↓フロント側でのエスケープ処理
' '
0x95 0x5c 0x27 0x27
 ↓データベース側の解釈
0x95 0x5c 0x27 0x27
0x95 \' で'一文字 ' がエスケープされずに余る

このように、末尾の「'」がエスケープされない状態となり、SQLインジェクション脆弱性が生まれる。

フロント側の日本語処理が不完全な場合

フロント側(言語処理系)の日本語処理が不完全な場合も「表'」の処理において「'」のエスケープ抜けが発生する

'
0x95 0x5c 0x27
 ↓フロント側でのエスケープ処理(0x5cと0x27をそれぞれエスケープ)
0x95 0x5c 0x5c 0x27 0x27
 ↓データベース側の解釈
0x95 0x5c 0x5c 0x27 0x27
「表」一文字 \'で一文字 ' がエスケープされずに余る

上記はShift_JIS固有の現象であるので、できるだけShift_JIS以外の文字エンコード、例えばUTF-8を使うとよい。しかし、ケータイブラウザのようにShift_JISのみ受け付けるものや、エンタープライズ系の応用では文字化けを避ける目的でShift_JISを要求される場合もある(入出力時に文字コード変換して処理はUnicodeに統一する手もあるが、わずらわしい場合もあるだろう)。

PostgreSQLの対応

PostgreSQLでは上記の問題に対応するために、バージョン8.1.4(2006年5月24日リリース)では、以下のような変更が行われた

  • 常にサーバ側で無効なコードのマルチバイト文字を拒否するように修正された
  • 文字列リテラル中の安全でない「\'」を拒否する機能が追加された
  • 以下略

上記については、ITproへの石井達夫氏による寄稿(【PostgreSQLウォッチ】第27回 SQLインジェクション脆弱性を修正,日本語ユーザーに大きな影響)が詳しい。簡単に要約すると、「\'」形式のエスケープを禁止・エラーにし、「''」方式(ISO標準)に限定できるようにした。これにより、Shift_JISの二バイト目の0x5cにまつわる「'」のエスケープ抜けを防止するというものである。但し、後方互換性の確保のため、backslash_quoteという設定パラメタが用意され、これが on の場合には、従来どおり「\'」形式のエスケープを許容する。

pg_escape_stringの挙動調査

冒頭に紹介した高木浩光氏のブクマコメントの後半は、自作のエスケープではなくDBで用意されたエスケープ機能を利用するようにという指摘であった。まことにその通りで、私のブログを読むと、自作のエスケープを推奨しているようにも読めるが、それは良くない。

それでは、PHPで用意されているpg_escape_stringは期待通り動作するのだろうか。簡単なスクリプトで検証してみた。

検証用スクリプト(PHP)
$cn = pg_connect("host=localhost user=xxxx password=xxx";
echo pg_escape_string($cn, "表\\'");

実行結果1 (standard_conforming_strings = on の場合)
表\''

実行結果2 (standard_conforming_strings = off の場合)
表\\''

PHP言語側のエスケープの都合で紛らわしいが、入力文字列は「表\'」である。前回紹介したstandard_conforming_stringsの設定を正しく反映して、onの場合は「表\''」(「\」のエスケープをしない)、offの場合には「表\\''」(「\」、「'」ともエスケープする)結果となっている。いずれの場合にも「'」は「''」とエスケープされるので、backslash_quoteの設定には依存しない。素晴らしい。

DBD::PgPPの場合

こんどはPerlでの例。PerlからPostgreSQLを利用する場合には、DBIとDBD::Pgの組み合わせが利用される・・・と思うのだが、筆者の環境では中々DBD::Pgがインストールできなかったので、代わりにDBD::PgPPを使って検証してみた。PgPPはピュアPerlで記述されたPostgreSQL用インターフェースである。DBD::PgPP中のquote()のソースを見ると、文字の変換部は以下のようになっていた(バージョン0.05)。

$s =~ s/(?=[\\\'])/\\/g;
return "'$s'";

正規表現の「?=」はゼロ文字の先読み表明というやつで、後ろに「\」か「'」が続くゼロ文字にマッチする。すなわち、「\」と「'」はそれぞれ「\\」と「\'」にエスケープされる。これはいただけない。standard_conforming_stringsもbackslash_quoteも無視されている。

これは恐らくDBD::PgPPの完成度があまり高くないということなのだろう。従って、自作のエスケープをせずにDBで用意されたエスケープ機構を使えというガイドラインは一般論として正しいと思うが、上記のような例もあるので、初めて使う前に簡単なテストをしておけば安心できる。

まとめ

  • 「\」のエスケープを要求するデータベースは日本語処理に特に注意
  • 例えば、Shift_JISを避ける
  • 自作のエスケープを避け、DBにて用意されたものを使う
  • その場合でも過信は禁物で、できるならチェックしてから使うとよい


2008-06-27 複文が利用できるデータベースの調査(2)

SQL Serverが狙われるにはまだまだ理由がある

徳丸浩の日記 - 複文が利用できるデータベースの調査 - SQL Serverが狙われるには理由があるにおいて、SQLインジェクションで別のSQL文を注入するためには、複文のサポートが必要で、SQLインジェクションの文脈でこれが可能であるDBMSはMS SQL ServerとPostgreSQLであることを示した。

ここで、複文を記述するには、セミコロン「;」で複数のSQL文を区切ると説明していたが、マイクロソフトの公表している文書によると、SQL Serverにおいては、複文の区切りにセミコロンは必要ないことが分かったので報告する

注意 複数の SQL ステートメントを区切るのに、セミコロンは必ずしも必要ありません。必要かどうかは、ベンダまたはインプリメンテーションによって異なりますが、Microsoft SQL server では不要です。たとえば、以下は SQL Server では 2 つの別々のステートメントとして解析されます。

SELECT * FROM MyTable DELETE FROM MyTable

[How To: ASP.NET で SQL 注入から保護する方法より引用]

なんてこったい。20年近くSQLを使ってきたが、複文の区切りでセミコロンが要らない(場合がある)なんて初めて知った。さっそく試してみた。

rs = st.executeQuery("select * from test update test set num2=7777");

見ての通りJavaで試したのだが、すんなり動いてしまった

同じようにPostgreSQLでも試してみた(こちらはPHP)が、エラーとなった。ベンダー依存ということだが、メジャーなDBでは、セミコロンなしで複文が書ける(プログラム中で)のはMS SQL Serverだけらしい。

こんなことを知っても実用的な意味はほとんどないのだが、私にとっては重大な成果があった。SQLインジェクション対策としてセミコロンを削除するよう説明している解説がある(例えばこれ)が、頻発するSQL Serverに対するSQLインジェクション対策としてのセミコロン削除はまったく意味がないことがはっきりしたことになる。

本日のリンク元 | 9 | TrackBack(0)

SQLインジェクション対策はおすみですか?
開発開始時点からのコンサルティングから、公開済みWebサイトの脆弱性検査、
脆弱性発見後の適切な対策まで
トップ 最新 追記