gallu’s blog

エンジニアでゲーマーで講師で占い師なおいちゃんのブログです。

静的プリペアドステートメントが「原理的に安全」な理由 後編っていうか本編

後半です。
…「完了編」まで引き伸ばさないように頑張っていきたい所存でございます (`◇´)ゞ
前回のは http://d.hatena.ne.jp/gallu/20111128/p1 をご覧ください。


さて。
まずは「ふつ〜にエスケープされた場合」に、SQL-Injectionが「発生しない」真っ当なケースを見てみましょう。
先ほどのSQLは、エスケープによって
UPDATE test SET a=10 WHERE id='\';GRANT ALL ON *.* TO cracker;--';
となりました。
入力された
';GRANT ALL ON *.* TO cracker;--

\';GRANT ALL ON *.* TO cracker;--
になった感じですね*1


では、同じくパースをしてみましょう。


→ U
→ P
→ D
→ A
→ T
→ E
→ (0x20):半角スペース
status:上書き


→ t
→ e
→ s
→ t
→ (0x20):半角スペース
status:上書き、テーブルはtest


→ S
→ E
→ T
→ (0x20):半角スペース
データーが 来るぞ〜〜!!


→ a
→ =
status:上書き、テーブルはtest、aに( )を上書く


→ 1
→ 0
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く


→ W
→ H
→ E
→ R
→ E
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(  )という条件に合致


→ i
→ d
→ =
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致


→ '
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致
現在「文字が入ってくる」モード


→ \
はぁ「エスケープ文字」ですかはいはいンぢゃ次の文字は気をつけて処理するよ〜
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致
現在「文字が入ってくる」モード+「次の一文字はエスケープ文字だよ」モード



→ '
ん…普通だと「文字が入ってくるモード 閉じ」なんだけど、今は「次の一文字はエスケープ文字だよ」モードなので、この子は「\'」になるから、一文字の「シングルクォーテーション」として取り扱うよ〜
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが'?????)という条件に合致
現在「文字が入ってくる」モード


→ ;
→ G
→ R
→ A
→ N
→ T
→ (0x20):半角スペース
→ A
→ L
→ L
→ (0x20):半角スペース
→ O
→ N
→ (0x20):半角スペース
→ *
→ .
→ *
→ (0x20):半角スペース
→ T
→ O
→ (0x20):半角スペース
→ c
→ r
→ a
→ c
→ k
→ e
→ r
→ (0x20):半角スペース
→ ;
→ -
→ -
→ '
うわまた長ぇなぁをい。
んと…「文字列の終端を意味するシングルクォート」の直前までの文字を足してくと…「';GRANT ALL ON *.* TO cracker;--」ってのが検索したい文字列なんだ。ほいさ了解。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが';GRANT ALL ON *.* TO cracker;--)という条件に合致


→ ;
ほい。SQL文終わりね。んぢゃ実行するよ〜」
「status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが';GRANT ALL ON *.* TO cracker;--)という条件に合致」を実行
statusを一端クリア。


→ \0
あ、終わった。


というわけで。
エスケープ処理によってめでたく、クラッカーが意図している「GRANT ALL ON *.* TO cracker;」は実行されずに終わりました。
めでたいですね。


ただ。この「エスケープ処理」も、うまいこと動かないケース(というかエスケープ漏れが発生するケース)ってのがあります。
では、そんな例を。
UPDATE test SET a=10 WHERE id='aa表';
ふつ〜に見えるSQLですので、そのまんまGo。エスケープしても特に変化はありません(罠1)。
っちゅわけで、パースいきます。


→ U
→ P
→ D
→ A
→ T
→ E
→ (0x20):半角スペース
status:上書き


→ t
→ e
→ s
→ t
→ (0x20):半角スペース
status:上書き、テーブルはtest


→ S
→ E
→ T
→ (0x20):半角スペース


→ a
→ =
status:上書き、テーブルはtest、aに( )を上書く


→ 1
→ 0
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く


→ W
→ H
→ E
→ R
→ E
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(  )という条件に合致


→ i
→ d
→ =
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致


→ '
ん? あぁ文字が入ってくるのね。了解。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致
現在「文字が入ってくる」モード


→ a
→ a
ちょっと一端ここで止めます。説明が細かいんで。ここまでは無問題でよいよね?
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaa?????という文字である)という条件に合致


「表」という漢字は、これがsjisの場合「0x95」と「0x5C」という2つの文字なので、ちと丁寧にパース処理を眺めてみます。


→ 0x95
文字として「0x95」が入ってきました。区別しやすいようにc(0x95)って書きます
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaac(0x95)?????という文字である)という条件に合致
現在「文字が入ってくる」モード


→ 0x5C
文字として「0x5C」が入ってきました。実は0x5Cって、\のことなんですね。
\なので「エスケープ文字」ですから、エスケープモードに移行します。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaac(0x95)?????という文字である)という条件に合致
現在「文字が入ってくる」モード+「次の一文字はエスケープ文字だよ」モード


→ '
ん…普通だと「文字が入ってくるモード 閉じ」なんだけど、今は「次の一文字はエスケープ文字だよ」モードなので、この子は「\'」になるから、一文字の「シングルクォーテーション」として取り扱うよ〜
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaac(0x95)'?????という文字である)という条件に合致
現在「文字が入ってくる」モード


→ ;
今は「文字が入ってくる」モードなので、;は普通の文字として扱うよ
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaac(0x95)';?????という文字である)という条件に合致
現在「文字が入ってくる」モード


→ \0
…あれ? 終わったの?
statusが空っぽじゃないから実行をしてみるよ?
「status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaac(0x95)';?????という文字である)という条件に合致」
ん…「対象レコード」の条件式が中途半端なままだから、処理できないからエラーにするよ〜


こんな風に。
色々条件にもよりはするのですが、「エスケープをしていたはずなのにしくじる」ケースなんてのもあります。
今回は「SQLがエラーになる」パターンにしましたが、これが容易に「クラックパターンに応用できる」のは、もうそろそろ書かなくても見えてくるころかなぁ、と思います。


結局のところ。
「完璧にエスケープが出来てりゃ」いいのですが、どうしても「現状想定できていない漏れ」の可能性が、否定できないので。
…いやまぁ別に「ボクは森羅万象過去現在未来のすべてを全知全能で知ってる」とかいう人は是非頑張っていただきたいのですが、おいちゃんはそんな器用な超常能力の手持ちがないので。
おいちゃんは、もちろん「今現在普通に既知」のものについては正しくエスケープが出来るとは思うのですが「未来の未知の脆弱性」にまで対応できている自信は、まったくないです。


まぁ一つに「随所に関所、で修正箇所を一箇所にまとめる」+「セキュリティ系のニュースのアンテナを鋭くしておく」ってのがあるのですが。
一方で「静的プリペアドステートメントによって、原理的に安全さを教授する」っていう方向性があるわけです。
(…やっと本題だよ)。


んぢゃ。とりあえずさっきこけていたSQLを使って、静的プリペアドステートメントの流れを見ていきましょう。
ちなみに「静的プレースホルダ」「バインド機構」とかなんか色々用語が見当たりますが、JIS/ISO の規格で「準備された文(Prepared Statement)」ってあるらしいので、とりあえずカタカナで「プリペアドステートメント」でいこうかなぁ、と。…長いんだけど(苦笑


まず。「静的プリペアドステートメント」では
SQLステートメント(プリペアドステートメント)
・バインドする配列
の2つを用意します*2


SQLステートメントはこんな感じ。
UPDATE test SET a=10 WHERE id=?;
クエスチョンマークんところに「バインドされたパラメタ」がはいります。


一方で「バインドする配列」には、今回は
aa表
を入れておきます。


では、この状態で同じようにパース処理を眺めてみましょう。


→ U
→ P
→ D
→ A
→ T
→ E
→ (0x20):半角スペース
status:上書き


→ t
→ e
→ s
→ t
→ (0x20):半角スペース
status:上書き、テーブルはtest


→ S
→ E
→ T
→ (0x20):半角スペース


→ a
→ =
status:上書き、テーブルはtest、aに( )を上書く


→ 1
→ 0
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く


→ W
→ H
→ E
→ R
→ E
→ (0x20):半角スペース
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(  )という条件に合致


→ i
→ d
→ =
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致


→ ?
ほい。バインドされたパラメタを使うのね。
バインド君、パラメタ一つ、持ってきて〜
 バ(*。・ω・)っ → aa表
ほいほいaa表って文字ね。了解。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがaa表)という条件に合致


→ ;
→ \0
終了系のパターンなので省略。


こんな感じ。
で…早々にで恐縮ではありますが、まとめ。


結局のところ。「本来はデータとして扱いたい」はずの文字列が「指示用の文字(SELECTとかGRANTとか;とか 0x20 とか)」と誤認されるのが、最大の問題になります。
クラックは「データのふりしてあの子 わりとやるもんだね と」指示用の文字を埋め込み、かつ、DBのパーサが誤認をするようなデータをぶち込んでくるわけです。
で、エスケープは「そういった誤認が発生しないように、特殊文字を"普通の文字として扱う"フォーマットに変換する」わけなのですが。
静的プリペアドステートメントは「そもそもとして、制御用の文字とデータとを、インタフェース的に分離してしまう」という、ある意味非常に、荒っぽいとすら言える決断をしたことで「そもそも入り口が違うから誤認のしようがない」状態を作り出したわけです。


このあたりが「静的プリペアドステートメントが「原理的に安全」な理由」なわけです。
…伝わったですかしらん?

*1:ここで「\'ぢゃなくて ''が正しくね?」と思ったかたは、とりあえず「あとの布石だから空気ヨメ!!」って発言を前提に、意見をオミットさせていただきます

*2:…用語がいまひとつ怪しいので、気付いた方は是非突っ込んでください