gallu’s blog

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

静的プリペアドステートメントが「原理的に安全」な理由

ものっそ大雑把に説明をしていきます。
「わかりやすさ」中心なので、「現在(ここほんの40〜50年くらい)の素晴らしい技術(アルゴリズム考え方アーキテクチャその他)」には大分と背を向けている可能性がありますのでご注意ください B-p
真面目にSQLのパースあたりとかを知りたかったら、是非具体的なソースコードを読んでみてくださいませ。


さて。
ざっくりと解説をするために、SQL文を非常に「簡単に」してみます。あちこち漏れてますが、その辺は適宜脳内補完をお願いいたします。
とりあえず、SQLには「以下の機能がある」と仮定します。


・読み書きどっち?
SELECTなのかINSERTなのかUPDATEなのか
・対象テーブル
どのテーブルに対する操作要求なのか
・対象カラムとか値とか
SELECTならナニを読みたいのか?
insertとupdateならどこにナニを書きたいのか?
・レコードに対する制限各種
whereとかその他。面倒なんでwhereのみってことで。


おいちゃんは「コンピュータの動きをそのまま真似て理解する」のが、古くからの伝統だと信じてますので。
ちょうど手元にあります一つのSQLを「素直に」パースしてみませう。
パースってのは、ようは「一本の文字列」を「使いやすいように切ったり張ったりする」ようなもんです。
で…記述文字数減らしたいんで。


→ a


って書いてあったら「aって文字が入ってきた」んだと思ってください。
では、早速


→ S
あぁ。selectですかねぇ?


→ E
うん、そんな感じっぽい。


→ L
あたりでしょう。

→ C
…C? ここはEでしょ? これじゃパース出来ないのでエラー終了ですよ!!


と。いきなりエラーパターンでした。
ちなみに入ってきたのは、こんな感じ。
SELCT * FROM test;
はいすみませんおいちゃんがよくやるtypoの一つです orz


では。真っ当な「SELECT * FROM test ;」を、ちと手間ですが、真面目にパースしてみませう。
文字列の終端は多分\0だとおもいますんで、そうしておきます。


→ S
→ E
→ L
→ E
→ C
→ T
→ (0x20):半角スペース
このタイミングで「あぁselectなんだなぁ」って思います。
status:読み


→ *
→ (0x20):半角スペース
ここで「あぁ、全カラムよこせですか。はいはい」って思うわけですね。
status:読み、全カラム


→ F
→ R
→ O
→ M
→ (0x20):半角スペース
次にテーブル名が来るようです。


→ t
→ e
→ s
→ t
→ (0x20):半角スペース
はぁ。「test」ってテーブル名ですか。安直ですねぇ。
status:読み、全カラム、テーブルはtest


→ ;
あ。SQLおわた。えと…「status:読み、全カラム、テーブルはtest」はいはい。
んぢゃってんで、testって名前のテーブルの全カラムを、順不同でreturnいたしやしょう。
「status:読み、全カラム、テーブルはtest」を実行。
statusはこのタイミングで一端空っぽになります。


→ \0
あ。文字列も終わりなんだ。
statusが空っぽなので、処理終了〜 ノ


こんな風に中では動いていきます(実行計画とかそーゆーあたりは今回まったく意識してませんので一瞬だけ頭からはずしといてください)。
同じように、update文を簡単に書いていきましょう。
UPDATE test SET a=10 WHERE id='t1' ;


→ U
→ P
→ D
→ A
→ T
→ E
→ (0x20):半角スペース
ほいさ。今回は「上書き」なのねん。
status:上書き


→ t
→ e
→ s
→ t
→ (0x20):半角スペース
またしても「test」ってテーブル名ですか。安直ですねぇ。
status:上書き、テーブルはtest


→ S
→ E
→ T
→ (0x20):半角スペース
ほいほい。この先にデータがくるのね。


→ a
→ =
はぁ。カラム名はaですか、また雑な…
status:上書き、テーブルはtest、aに( )を上書く


→ 1
→ 0
→ (0x20):半角スペース
書き込む値は10ですか。ほいほい。
status:上書き、テーブルはtest、aに10を上書く


→ W
→ H
→ E
→ R
→ E
→ (0x20):半角スペース
ん? WHERE? ほいほい。対象レコードに条件を指定したいのね。よろしくってよ?(いきなりキャラ変えない
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(  )という条件に合致


→ i
→ d
→ =
検索用の対象カラムはidって名前なんだ。ほいほい。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが  )という条件に合致


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


→ t
→ 1
→ '
t1が入ってきて「文字が入ってくるモード」終了。
ってことは「idがt1って値のレコード」を対象にしたいんだね。
status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idがt1という文字である)という条件に合致


→ ;
→ \0
おわた系のパターンなので省略。


すげー大雑把に、こんなことやってます。
ちなみに、文字をシングルクォートで囲むのはまぁ「それが必要だから」。
簡単に、0x20で説明。


部分的に切り出して
id='t1'
の形式を見てみませう。
今度は「idがホワイトスペースを含む文字列 "t 1"であるレコードを探したい」場合。
まずは正しい形式である「 id='t 1' 」をパース。


→ i
→ d
→ =
検索用の対象カラムはidって名前なんだ。ほいほい。


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


→ t
→ (0x20):半角スペース
※半角スペースあるけど、今は「文字が入ってくるモード」だから、この半角スペースは「文字として」扱うよ〜
この時点で、以下の感じ
status:対象レコードは(idがt ???)という条件に合致
???には、きっとこれから「何か」が入ってくるはず〜


→ 1
→ '
t 1が入ってきて「文字が入ってくるモード」終了。
ってことは「idがt 1って値のレコード」を対象にしたいんだね。


ってな感じで恙無く終了。
で、もしシングルクォートがないと


→ i
→ d
→ =
検索用の対象カラムはidって名前なんだ。ほいほい。


→ t
→ (0x20):半角スペース
tが入ってきて「一区切り」を意味する半角スペースが入ってきたから、探したいのは「idがtのときなんだね」…あれ?


って誤認しちゃう。
だから、シングルクォートが必要なんだよ、ってのは、余談というか前置き。
実はあとですげぇ重要になるから、さりげなくチェック。


んでは。
先に、悪名高いいわゆる一つの「SQL-Injection」を、見ていきましょう。
とりあえず、状況。


UPDATE test SET a=10 WHERE id='{GETパラメタのデータ}' ;
って感じで、本当はパラメタに「t1」とかが入ってきて
UPDATE test SET a=10 WHERE id='t1' ;
とかを想定していたんだけど、なんか悪くてずるくて黒い人がパラメタに「変な値」を入れてきちゃった、的な状況。
「変な値」とかを知る由もなく、素直にパース作業に移ってみましょう。
ところどころはしょりますんで、その辺ご注意を。


→ 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が空っぽ)という条件に合致
「文字が入ってくる」モード解除


→ ;
SQLおわり。
「status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが空っぽ)という条件に合致」を実行
statusを一端クリア。


→ G
→ R
→ A
→ N
→ T
→ (0x20):半角スペース
GRANTね。ほいほい「DBの権限を設定/変更/削除」するのね。
status:RDM権限設定


→ A
→ L
→ L
→ (0x20):半角スペース
allですか全権限付与ですか。豪勢ですねぇお大臣ですねぇ。
status:RDM権限設定、あらゆる権限を付与


→ O
→ N
→ (0x20):半角スペース
この先に対象テーブルと対象database名が来るのね。ほいほい


→ *
→ .
→ *
→ (0x20):半角スペース
*.*ってこたぁ「あらゆるdatabaseのあらゆるテーブルに対して」の権限ですか。どっかの皇帝みたいだねぇ
status:RDM権限設定、あらゆる権限を付与、すべてのdatabaseのすべてのテーブルが対象


→ T
→ O
→ (0x20):半角スペース
この先に「誰がこの権限もらえるのか」を書くのね


→ c
→ r
→ a
→ c
→ k
→ e
→ r
→ (0x20):半角スペース
ユーザ名はcrackerさん、っと…イヤな名前だねぇまったく。
status:RDM権限設定、あらゆる権限を付与、すべてのdatabaseのすべてのテーブルが対象、ユーザ名はcracker


→ ;
SQLおわり。
「status:RDM権限設定、あらゆる権限を付与、すべてのdatabaseのすべてのテーブルが対象、ユーザ名はcracker」を実行…本当にいいのかしらん?


→ -
→ -
あら。--がきたよ。改行コードが来るまでの間、以降コメントだねぇ。
現在「コメント」モード


→ '
→ ;
→ \0
おやコメントモードのまま終わっちゃったよ。
んじゃ、statusも空っぽだし、何もしないで終わるよ〜。


さて、やったことを振り返ってみましょう。
・「status:上書き、テーブルはtest、aに10を上書く、対象レコードは(idが空っぽ)という条件に合致」を実行
・「status:RDM権限設定、あらゆる権限を付与、すべてのdatabaseのすべてのテーブルが対象、ユーザ名はcracker」を実行
の2つです。前者はともかく、後者のほうは「なにしとんじゃ (#゚Д゚)ゴルァ!!」な感じですね(GRANT文、微妙に「どこに行ってもエラーになるように」微妙に書式を変えているつもりですが…通ったらごめんなさい)。


このSQL-Injectionですが、入力されたのは「';GRANT ALL ON *.* TO cracker;--」って文字列を想定しています。
そうすると
UPDATE test SET a=10 WHERE id='';GRANT ALL ON *.* TO cracker;--';
っていうSQLになりまして…まぁ、上述のような「大惨事」が発生します。


…ごめん体力が尽きたから、後編に続く orz


追記
後編 → http://d.hatena.ne.jp/gallu/20111129/p2