出力先のシステムが同じでも、出力先が異なる場合はよくあります。これを意識していないとセキュリティ問題の原因になります。
私は普段から出力を行う場合には、どのような出力先であるか、を常に意識しています。「大垣はなぜエスケープにこだわるのか?」と疑問に思っている方も居ると思います。その理由は出力にこだわっているからです。安全なプログラミングには出力先を良く理解する事が必須だからです。フレームワークやライブラリは出力先の特性を良く理解していなくても、安全に出力できるような仕組みを提供している場合もあります。しかし、前提条件があったり、不完全な場合も多いです。フレームワークやライブラリが完全であるかないか?を確認・理解する為にも出力先と出力先に対する適切なエスケープ/バリデーションにはこだわる必要があります。APIを利用した出力を行っている場合、そのAPIが出力先(出力先の抽象化レイヤー)だと考えると分り易いと思います。
これは同じ出力先?
出力先は同じシステムやプロトコルですが、同じ出力先でしょうか?
SMTP(メール)
- メールヘッダー
- メールボディ(本文)
HTTP
- HTTPヘッダー
- HTTPボディ
LDAP
- DN
- フィルター
- クエリ語句
SQL
- パラメーター(リテラル)
- 識別子
- SQL語句
HTML
- コンテンツ
- タグ
- 属性
- URL
- CSS
- JavaScript
これらは出力先が同じでも、どこに出力しているのかによって異なる出力先である例です。「異なる出力先」とは異なる出力ルールを適用しないと、安全性やシステムの正しい動作が担保できない出力先です。HTMLコンテンツにはHTMLエスケープが必要であり、URLにURLエスケープ(URLエンコード)が必要であることは良く理解されていると思います。上記の例では「属性」とひと括りにしていますが、更に細かく意識しないとなりません。例えば、HTMLタグの属性名と属性値は区別しなければなりません。属性値もhrefやsrc属性には他の属性と異なるルールがあります。HTMLが利用するHTTPプロトコルは様々なデータの出力に利用されます。HTMLなのか、テキストなのか、JavaScriptなのか、CSSなのか、など同じ出力先ですが、出力する物が異なると異なるルールが適用されます。
1つの出力先(ブラウザ、RDBMS、LDAP、HTTPなど)だからといっても、どの部分に出力しているかによって注意しなければならない項目やエスケープ方法が変わります。ブラウザの場合は更に複雑で、ブラウザのパーサーでパースされるコンテクストであるかないか、も正しい出力には欠かせません。(参考:JavaScript文字列のエスケープ)
出力先の入力仕様がどうなっているか?APIを使って出力している場合はAPIの入力仕様と出力仕様がどうなっているか?理解していないと安全なシステムは作れません。出力先に直接出力することは「危険である」と認識し、セキュリティ対策が不必要なAPIを利用している場合でも、その制限や仕様を知り、エスケープもAPIも利用できない場合はバリデーションが必要である事を理解していなければなりません。
良いコーディングルール、悪いコーディングルール
全ての開発者が出力先の入力仕様、APIの入出力仕様を完全に把握すべき、とは言いませんが少なくともセキュリティを担当する開発者は「安全な出力には何が必要か?何が前提条件か?」を理解していなければなりません。理解した上で、他の開発者が安全なプログラムを書けるようコーディング規約などを整備する必要があります。
セキュリティを担当しない開発者もコーディングルールがどうなっているかを理解しておくことは当然ですが、コーディングルールから外れる場合には「その出力・APIの使い方は安全か?」を意識する必要があります。コーディングルールは出力先を明確にし、開発者がルールから外れた処理を行おうとした場合に分かるように記述すべきです。
悪い例
SQLクエリを実行する場合は必ずプリペアードクエリ(プレイスホルダ)を利用する。
良い例
SQLクエリを実行する場合は必ずプリペアードクエリ(プレイスホルダ)を利用し、変数はプレイスホルダ以外に利用しない。
プリペアードクエリを安全に利用するには、クエリに利用する変数が全て、パラメータ(SQLのリテラル)として利用されていることが前提条件になります。詳しく説明する必要はないですが、それ以外のケースは別のセキュリティ処理が必要であることが分かるようになっているべきです。悪い例ではプレイスホルダ以外の場所に変数を埋め込みたいケースを禁止していません。セキュリティ対策はできるだけコンパクトである方が良いですが、例外があり、その例外をシステム的に禁止できないのであればルールに記載しなければなりません。エスケープ/バリデーションしないと安全に出力できない、と判るルールになっていなくても構いませんが例外の処理である事が判るようになっていなければなりません。
悪い例
HTMLタグを含む出力をする場合、必ずHTMLタグヘルパーを利用する。
良い例
HTMLタグを含む出力する場合、必ずHTMLタグヘルパーを利用し属性値/内容のみに変数を利用し、それ以外に変数は利用しない。
HTML出力ヘルパーメソッド・関数には任意の内容、属性値、属性名を設定できる物が多いですが、効率を良くするために属性名のバリデーションを行っていない物も多くあります。このようなフレームワークを利用している場合、信用できない入力値が属性名に与えられるとJavaScriptインジェクションが可能になります。プリペアードクエリと同様に使わせたくない利用方法が使える場合は明示的に禁止しなければなりません。
本当の出力先が何であるか意識する
セキュリティがよく分からない!という新人プログラマであっても「出力先のシステムが同じでも、出力先が異なる」を意識し「異なる出力先には異なるルールがあり、それを守らないと危険である」と認識するだけでも危険なコードを書く可能性は随分減ると思います。
SQLやLDAP、HTMLなどをより安全に利用できるようAPIが用意されている場合も多いでしょう。アプリケーションプログラマにとっての基本的な出力先はこれらのAPIである事も多いと言うことです。APIが用意されている場合、抽象化されている分、そこに潜むセキュリティ問題が隠蔽され分かりづらくなっています。APIも出力先であり、セキュリティ担当者はその制限や例外は理解し、それ以外の開発者に制限・例外がある事を周知させる仕組みが必要です。
全ての出力先に利用可能な安全性維持の基本原則
この原則はこのブログで何度か書いたと思いますが、もう一度書きます。
アプリケーションを構築する場合、多種多様な出力先があります。それぞれに必要なセキュリティ対策があったり、出力を抽象化してプログラマがセキュリティ対策を意識しなくても構わない仕組みを提供しています。これはら利用するインフラ(言語など)やフレームワーク(Webアプリフレームワークなど)によって異なります。しかし、安全な出力に必要な基本原則は変わりません。
- 出力先の入力仕様に従いエスケープして出力する
- 利用可能な安全なAPIを利用して出力する
- エスケープもAPIも利用できない場合はバリデーションする
出力先の正しい入力仕様(エスケープ)を知ることはセキュリティ対策として最も重要です。安全に出力できるAPIが提供されている場合でも、実装が正しいか、実装の制限は何か、を理解するためには出力先の正しい入力仕様(エスケープ)を知る必要があるからです。エスケープもAPIもできない場合はバリデーションを行います。
SQLのプリペアードクエリの様に、セキュリティ対策としても利用できるAPIが出力先のシステムで用意されている場合もありますが、用意されていない物も多くあります。APIが用意されていても、別の方法で出力できてしまう場合もあります。APIを使えばOK、とする考え方では様々なセキュリティ問題を作る原因になります。実際の開発に利用するコーディングルールでは出力先によって「API > エスケープ > バリデーション」の順番になる場合もありますが、セキュリティを維持するための重要度は「エスケープ > API > バリデーション」であることは変わりません。
何も考えず(処理せず)出力して構わない出力先もありますが、そのような出力先ばかりではないと考えなければなりません。むしろそのような出力先は例外である、と考えるべきです。例えば、出力先が自由に出力できるファイルであったとしても
- テキストファイルであれば文字エンコーディングが正しいか?
- バイナリであっても適切な形式であるか?
- 制御文字が含まれていても構わないか?
- 出力するデータサイズは妥当か?
などチェックする項目(保証する項目)が全くないとは限りません。
安全なコードを書くためにこんな事に注意している、という事項がありましたらコメント頂けると有難いです。
出力先毎にどう出力すれば安全に出力できるのかをこれからも書きますが、「適切な出力」の重要性を理解して頂くたために解説する必要があると思ったのでエントリを作りました。
PHPのセキュリティ入門書に記載するコンテンツのレビューも兼ねてPHP Securityカテゴリでブログを書いています。コメント、感想は大歓迎です。