技術評論社の「Web Site Expert 」誌に、Webアプリケーション・セキュリティ・フォーラム関係者の持ち回り企画「WASF Times」が連載されている。私の番も回ってきたので昨年9月発売号に寄稿させていただいた。近頃はサニタイズ言うなキャンペーンもだいぶ浸透してきたようだし、もういまさら不要という気もするが、以下、その原稿を編集部の承諾のもと掲載しておく。
Webアプリを作ったらセキュリティ屋に脆弱性を指摘された――そんなとき、「入力をサニタイズしていない」なんて言われたことはありませんか?
「入力」というのは、ブラウザから送信された情報をCGIパラメータとして受信した値のこと。これを「サニタイズしろ」というのです。なんでそんなことしないといけないの?プログラムの内容からして必要のないことなのに?
そう。必要ありません。「それがセキュリティなのだ」「セキュリティのためには当然」なんてセキュリティ屋に言われたら、その人はもう信用しなくてよいです。
具体的にはこうです。CGIパラメータが「p」という配列変数に入ってくるとしましょう。こんなコードを思い浮かべてください。
p[i] = urlDecode(p[i]) p[i] = charcodeConv(p[i]) p[i] = sanitize(p[i])
URLは「%xx」でエンコードされていますから、まずこれをデコードします。そして文字コードを統一するため文字コード変換します。ここまではミドルウェアが自動的に行う場合もあるでしょう。定型的な処理です。この直後に、「サニタイズ処理」が入っています。これが「入力のサニタイズ」です。
サニタイズ処理の内容はこうです。「クロスサイトスクリプティング」脆弱性を例にすると、この脆弱性では「<」や「>」などの文字が原因となります。そこで、「入力のサニタイズ」によって、「<」の文字を削除してしまうとか、全角文字「<」に変換するとか、「<」に変換するという処理が「サニタイズ」だということになります。
しかし、このような処理は必要ではありません。なぜなら、クロスサイトスクリプティング脆弱性を排除するには、HTML文字列を書き出す部分で、「<」などを「<」などに変換(エスケープ処理)すればよいからです。
ちぽっけなCGIプログラムならどっちでもよいでしょう。数行のプログラムなら、入力の直後で処置するのも、HTML出力の直前で処置するのも同じことです。
x = p[i] //入力 x.escape() //処置 print x //出力
しかし、大規模なプログラムではどうでしょうか。入力から出力までのデータフローは長くて複雑なパス(道のり)を構成します。真ん中あたりで処置するのは最悪です。処置されているかいないかの確認が難しくなるからです。そうすると、入力の直後か出力の直前ということになります。そして、入力の直後がよくないのは以下の理由からです。
入力の時点では何をすればよいのかは明白ではありません。「<」を「<」に変換する必要があるのは、その値がHTML出力で使われるという理由からです。その入力値が、もしSQL文の一部として使われるならば、SQLインジェクション対策をしなくてはなりませんから、「'」の文字を処置しないといけません。SQL文にも使われるしHTML出力にも使われるなら、両方処置しないといけないということになってしまいます。両方処置した値がHTML出力に使われたとき、無駄に「'」がエスケープされて「''」とか「\'」と表示されてしまうという、マヌケなことが起きます。入力直後でサニタイズするというのは、プログラムのロジックとして誤りです。
ではなぜそんなことを言うセキュリティ屋がいるのでしょうか。ひとつには、プログラマでないセキュリティ屋がいるからです。Webアプリの脆弱性検査の一部は、プログラミング能力がなくてもできます。攻撃に使われるパターンを入力に与えて結果を見る。脆弱性についての知識さえあれば、欠陥があるという事実の指摘はできるわけです。
コードレビューによる検査ならば、さすがにいくらかプログラミング経験があるはずです。しかし、洗練されたプログラマとは限りません。コードを追いかけることさえできれば、脆弱性は指摘できます。「洗練されたプログラマ」ならば、「コードをどの位置に書くか」にこだわりを持っているはずです。プログラムの見通しをよくし、メンテナンス性を高めるためにです。そのセンスがあれば、必要なところの直前にエスケープ処理を施すのが正しいことはわかるでしょう。
「入力は一か所だから対策が楽だが、出力するところは無数にあるから対策漏れが起きる。だから入力で処置するべきなのだ」と主張する人がいます。しかし、入力はCGIパラメータだけとは限りません。データベースを検索して得た値も該当します。それが多数あるなら対策漏れが起き得るのは同じことです。
「CGIパラメータの入力直後で全部サニタイズしていれば、データベースにもサニタイズ済みの値が入るはずだから大丈夫だ」という主張は誤りです。「セカンドオーダーSQLインジェクション」という攻撃手法が知られています。そんなことはどうでもよいのです。出力(SQL文を構成するところ)でエスケープするという正しいコーディングをしていれば、セカンドだろうがサードだろうが気にする必要がありません。
洗練されたプログラマでないセキュリティ屋は、「入力の全部にサニタイズが必要」などと言います。処置する場所が出力の直前であっても、「入力からの変数にサニタイズ」などと言います。外部からの入力に依存した値(データフローがつながっている値)について処置せよというのです。そんなことは考える必要がありません。出力で使う値の全部に処置すればよいのです。
たとえば、「<h1>...</h1>」というHTMLを出力するコードで、「...」の部分に変数「title」の値を差し込もうというときに、変数「title」が外部からの入力に依存しているかどうかなんてことは考えず、常に「<」と「>」と「&」のエスケープ処理をするということです。
「外部からの入力に依存していない値については(セキュリティ上問題ないので)エスケープを省略する」という発想は、利益がない(いまどきそんな省略で性能に変化が出ることはない)ばかりか、メンテナンス性を悪くし、脆弱性の原因となります。最初に書いた時点では入力に依存していなかった式が、プログラムの別の場所の変更によって、入力依存になるかもしれないからです。
HTML出力するときに「"」を全部「"」に変換せよと、セキュリティ屋が言うことがありますが、これもちょっと正確ではありません。たとえば、「<a href="...">」という出力をするコードで、「...」部分に式の値を埋め込むときには「"」のエスケープが必要になりますが、それは「"」で囲まれた中に埋め込もうとしているからであって、それ以外の場所で「"」をエスケープすることは必要ではありません。
HTMLは文法を持った言語ですから、「"..."」と引用符で囲まれた中に式の値を差し込むときは、その式に「"」が含まれていればエスケープするのはプログラム上もともと当然であって、セキュリティが理由ではありません。これを怠るとたまたまセキュリティ脆弱性にもなるというだけです。
では、セキュリティ屋たちが「入力をサニタイズ」と言うようになってしまった背景は何でしょうか。
ひとつには、既に組みあがったWebアプリに脆弱性が発覚したときに、応急処置として脆弱性を排除するには、CGIパラメータの入力時点で「サニタイズ」する方法が簡単である(出力の全部を漏れなく直すのには時間がかかる)ため、そのような対策が一時期普及したのが背景にあると考えられます。(しかし、先に述べたように、入力はCGIパラメータだけではありません。)
入力値の検査をするべきだという主張もよく耳にします。電話番号が入るはずのパラメータに対しては、数字とハイフンだけからなることを検査して、合格のときだけ処理を実行するというものです。たしかに、これが結果的に脆弱性対策になる場合もあります。しかし必ずなるとは限りません。これは、アプリケーションが要求している「validation」処理であって、サニタイズとは別の機能だと考えるべきです。
もうひとつはPerlです。20世紀には、CGIといえばPerlという時代がありました。open文にファイル名を渡すときに、コマンドとして起動されてしまうような書き方が広まり、これがOSコマンドインジェクション脆弱性となりました。これを出力時のエスケープ処理で処置するのは簡単ではありません。OSのシェルの記号解釈の文法が複雑だからです。そのため、記号を全部削るという対策が普及しました。英語圏ではこれを「sanitize」と呼ぶようになり、日本にも持ち込まれました。(これも、シェルが起動するようなopenの書き方をすべきではないというのがより的確な対策です。)
私は昨年から、「サニタイズ」という言葉を使うのはもうやめようと主張しています。入力での処置を連想させるからです。すると、出力部分で適切にコーディングすることを指して「それをサニタイズと言うのだと思っていました」と言う人が出てきました。そこまで「サニタイズ」の語義を広げるなら、もはや「サニタイズ」という言葉は、「セキュリティをちゃんとする」という程度の意味しかありません。それなら「サニタイズ」という言葉を使う必要がありません。「セキュリティをちゃんとする」と言えばよいのです。
その一方で、サニタイズという発想がセキュリティ屋たちの思考を蝕んでいる実態があります。なんでも「サニタイズしていないのが原因」と書けば説明できた気になるという症状です。たとえば、JPCERT/CCが公表した「JPCERT/CC REPORT 2006-01-12」という文書には、「Perlのフォーマット文字列処理の脆弱性」として、次の記述があります。
この問題は、Perlで書かれたプログラムを修正し、外部から入力されたフォーマット文字列をそのまま処理しないようにすることで解決します。
これは間違った解決策です。フォーマット文字列はできるだけ定数にしておくべきというのが正しいのですが、JPCERT/CCは、「%」をエスケープせよとでも言い出しそうです。この記事は英語版の記事を翻訳したもので、原文では「外部から入力されたデータをフォーマット文字列に含めない」と書かれていて、正しい解決策を示しているのに、JPCERT/CCでは誤訳になっています。
これを書いたセキュリティ屋は、「入力をそのまま処理しない」と言っておけば対策を示したことになると、もはや癖になっているのでしょう。「サニタイズ」という言葉を「そのまま処理しない」に言い換えただけです。
重要なことは、どんなコーディングが正しいのかをコードの場面ごとに個別に理解し、説明することです。それができない(つまりプログラマでない)セキュリティ屋にとっては、「サニタイズ」は万能で便利な言葉なのでしょうが、プログラマが使う言葉ではありません。
コードの汎用性を常に意識して、仕様に忠実な実装で、メンテナンス性の高いエレガントな記述を実施していれば、本来いくつかの脆弱性*1は生じないはずのものです。セキュリティ屋の言う「セキュリティのためのセキュリティ」に惑わされず、何が本来的に正しい書き方なのかを考えていきたいものです。
詳細は「サニタイズ言うな」で検索して熟知すべし。
*1 転載時註: CSRFなどはこれに含まれない。(参照:「クロスサイトリクエストフォージェリ(CSRF)対策がいまいち進まなかったのはなぜか」)
[http://kaede.to/~canada/doc/sanitize-perl 前にも書いたが]、HTTPにおけるOSコマンドインジェクション脆弱性は、僕が知る限りではPerlスクリプトではなく、”’Cプログラム”’における実装で有名になったと記憶している。
...
最近、「セキュリティ対策はきりがない、いたちごっこになるだけじゃないか。」という声を耳にすることがよくあります。年々新しい攻撃手法が発見されるため、いつまで経っても安心できない、というのが理由のようです。 ただ、正しく実装している限り、いたちごっこにな..
高木氏が再びサニタイズ言うなキャンペーンの解説記事を掲載した。 高木浩光@自宅の日記 - WASF Times版「サニタイズ言うな!」 今回のはかなり判り易く書かれていると思いますが、それでもまだ理解に欠ける人々もいるようで、アルファブロガーの宿命か?高木氏もたいへ..
WASF Times版「サニタイズ言うな!」 by 高木浩光@自宅の日記 に 終わりなきサニタイズ言うなキャンペーン というトラックバックがついている。 このエントリを読むとエスケープという処理
サニタイズ言うなキャンペーンというのがある。しかし,このキャンペーンは有効に働いていないように思える。分かりにくいからである。分かりにくい原因はサニタイズの定義がきちんとなされていないからである。...
ふと気が付いたのだが、高木さんのこの記事の考え方は、畑村氏が「危険学のすすめ」で示した本質安全に相当近い。(適宜覚書はてな異本 - 「危険学のすすめ」の感想) しかし、このような処理は必要ではありません。なぜなら、クロスサイトスクリプティング脆弱性を排除..
例の納豆のやらせ問題…結構話題になりましたよね。
スーパーからは納豆が消えたらしいですし。
とはいえ、納豆って普通に良いものですし、毎日食べたら体にいいはずなんです