何故かあたり前にならない文字エンコーディングバリデーション | yohgaki's blog
ってあるように、いまいち文字コードの不正な判定による危険性ってのが分かってない。
SJISの問題は、(2/3)SQLインジェクションを根絶!セキュア開発の極意 - 第5回■注目される文字コードのセキュリティ問題:ITproの記事がわかりやすかった。
というか、やっぱりPHP使ってると誰でも一度は「なんじゃこの『¥』は?」って思うもんなんで。
なるほど、確かに↓の図のように「あるバイト」が2つの意味を持つっていう文字コード形態はやばいんだなと。
EUC-JPはそんなことはしないで、1つのバイトには1つの意味しか取らせない。
だけど、これでも文字化けが起こることがある。経験的には、「マルチバイトをXX文字で切り落としたい」とかやった場合。ちゃんと文字コードを判定してくれるPHPでいえばmb_substr何かを使えば問題ないけど、バイト数で切っちゃうsubstrだと、奇数文字で切っちゃう時は1文字が半分にちぎれる可能性がある。
んー、じゃあUTF-8は何が危ないの?
そのあたりの問題とか、他言語で文字マップ領域が被るっていう問題を解消したのがUincodeで、その実装の一つであるUTF-8なら問題ないんじゃないの〜っていう感じなんだけど、それは違うみたい。
それが書かれた記事が「第4回 UTF-8の冗長なエンコード:本当は怖い文字コードの話|gihyo.jp … 技術評論社」っていう記事。
この記事を指して最初に引用したohgakiさんのブログでは
このよう事は簡単に理解できると思っていたので
と書かれているが、これ全然簡単じゃないよ・・・って感じで色々考えてみたのがこのエントリ。(前置き長っ)
あくまで、「そうなんじゃないかな〜」って自分が考えただけなんで、間違ってたら突っ込みなどどうぞ。
そもそも、UTF-8ってどうやって文字を判別してるのか?
UTF-8は、マルチバイト1文字を表すのに、1バイト〜4バイトの可変なバイト数領域を使う。
固定にすれば良いのに、何でそんな面倒なことするんかっていうと、ASCII文字と互換性を持たせるためらしい。
で、第4回 UTF-8の冗長なエンコード:本当は怖い文字コードの話|gihyo.jp … 技術評論社によると、
Unicode文字範囲 | UTF-8でのバイト列 |
---|---|
U+0000〜 U+007F | 0xxxxxxx (00〜7F) |
U+0080〜 U+07FF | 110xxxxx 10xxxxxx (C2〜DF) (80〜BF) |
U+0800〜 U+FFFF | 1110xxxx 10xxxxxx 10xxxxxx (E0〜EF) (80〜BF) (80〜BF) |
U+10000〜 U+10FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx (F0〜F7)(80〜BF)(80〜BF)(80〜BF) |
ってなってる。
自分が、「単に1と0のデータを文字として出力するマシン」になったつもりで、「このデータはUTF-8の文字列ですよ」ってデータが来た場合に
「あ、これは『あ』だ」
「これは、『\』だ」
とか見分けるために、「何バイトで1文字を表現してるのか」を判定するにはどうしたらいいかを、↑の表を見て考えてみた。
すると、UTF-8はすこぶる簡単に「これは3バイトで1文字だな」とかが分かるようになっている。
- 最初の1バイトの1ビット目が0なら「1バイトで1文字」。
- 最初の1バイトの1ビット目と2ビット目が1なら「2バイトで1文字」なので、次の1バイトも合わせて1文字を出力する。
- 最初の1バイトの1〜3ビット目が1なら「3バイトで1文字」なので、次と次の次の2倍とも合わせて1文字を出力する。
という感じ。
つまり、最初の1バイトは「何バイト一緒に食えばいいか」というのを教えてくれてる。
そして、「10」のビットで始まるバイトは「文字の中間」を構成するバイトということになる。
ところで、上のテーブルは110xxxxx (C2〜DF)ってなってるけど、C0〜DFじゃないかな?)
冗長なエンコードとは?
じゃあ、冗長なエンコードって何?ってことなんだけど。
それは「本来なら1バイトで済むデータを3バイト必要なように見せる」っていうことだと思う。
その例として、第4回 UTF-8の冗長なエンコード:本当は怖い文字コードの話|gihyo.jp … 技術評論社によると、「/」を挙げているのだけど。
正しいエンコード | 0x2F |
---|---|
不正なエンコード | 0xC0 0xAF (2バイト表現) 0xE0 0x80 0xAF (3バイト表現) 0xF0 0x80 0x80 0xAF (4バイト表現) |
何でこんなんが全部「/」として認識されるんだ!?って感じだけど、実際にやってみる。PHPで。
<?php echo $sl1 = hex2bin('002f')."\n"; echo $sl2 = hex2bin('c0af')."\n"; echo $sl3 = hex2bin('e080af')."\n"; function hex2bin($hex_str) { return pack("H*" , $hex_str); }
ってやってコマンドラインから実行してみる。ターミナルの出力はUTF-8。なので、ターミナルが「UTF-8である」と解釈された文字が私の目に映るはず。
じゃあ、これが本当にバイナリとして同じデータなのかどうかを確かめてみる。簡単に、別の文字コードに変換してみればいい。
EUC-JPに変換して出力してみると、
ってなった。
つまり、最初の「/」は本物の1バイトのスラッシュだけど、後の2つは1バイトのスラッシュではないということ。
SJISに変換しても
となる。
これは何が悪いのかというと、それは「変なUTF-8エンコードを見抜けないターミナル」(このキャプチャの場合はTeraTermPro UTF-8版)なんだけど、世の中にはそういうアプリが多いらしい。それはブラウザも多分にもれず、FirefoxやI.Eも同じ勘違いをしてしまうらしい。
なんでそんな勘違いをするの?
じゃあ、何でそんな勘違いを?って感じだけど、それはなんとなくだけど・・・
第4回 UTF-8の冗長なエンコード:本当は怖い文字コードの話|gihyo.jp … 技術評論社にある
の絵を見て、「0xE0 0x80 0xAF」を処理するときの動作として。。。
Firefox「現在の文字コードはUTF-8。次のバイトは、0xE0 ・・・1110....とすると3バイトで1つの処理だな」
Firefox「3バイトを1つに並べてみると、1100000 10000000 10101111・・・・」
Firefox「???最初の2バイトは意味のある部分のビットが全て0・・・最後の1バイトは『0101111』これは『00101111』を表してるのかな?」
ということなんだろうか?
この辺はよーわかりませんが、なんとなくそんな感じかも。
これがXSSにつながるって言うのは
要するに、「3バイト食いますよ〜」っていうフラグを立てておきながら(つまり1100000のバイトを送る)、2バイトしか送らなかった場合、次の文字も巻き込んでしまうんじゃないかってことかな?これって、EUC-JPやSJISであったのと同じ問題で。それがUTF-8でも起きるっていう。
そうすると、HTMLの閉じタグとかクォートを食っちゃえばXSSが簡単に成立するてことで。
しかし、これを「このよう事は簡単に理解できると思っていたので、」と言っちゃうohgakiさんには
r ‐、 | ○ | r‐‐、 _,;ト - イ、 ∧l☆│∧ セキュリティ エバンジェリストの諸君! (⌒` ⌒・ ¨,、,,ト.-イ/,、 l われわれPHPerにとって大切なのは |ヽ ~~⌒γ⌒) r'⌒ `!´ `⌒) すぐ使えるサンプルか、コピペで動くコードだけだ。 │ ヽー―'^ー-' ( ⌒γ⌒~~ /| PHPerを買いかぶらないでもらいたい │ 〉 |│ |`ー^ー― r' | │ /───| | |/ | l ト、 | | irー-、 ー ,} | / i | / `X´ ヽ / 入
と、言いたい。