なぜPHPアプリにセキュリティホールが多いのか?

第44回セキュリティ対策が確実に実施されない2つの理由

セキュリティ対策は言語やアプリケーションを問わず非常に重要です。しかし、取るべきセキュリティ対策が確実に実施されないケースが広く見受けられます。

最近の例では次のような物があります。

上のURLの脆弱性も対策が簡単なものが多いですが、対策が簡単なSQLインジェクションの脆弱性も数多く見つかっています。

なぜ簡単な対策で防げる脆弱性でもセキュリティ対策が確実に実施されないのか?それには理由があります。

その理由とはこの2つではないでしょうか。

  • セキュリティ対策とコーディングのベストプラクティスは相反することを理解していない
  • セキュリティ対策の基本中の基本を理解していない

セキュリティ対策とコーディングのベストプラクティスは相反する

セキュリティ対策とコーディングのベストプラクティスは真っ向から対立している部分があります。

  • 多重のセキュリティ対策を実施 vs 重複処理を排除した効率よいコード記述

このセキュリティ対策とコーディングのベストプラクティスは対立する考え方で、どちらも「正しい」です。これが問題を大きくしている一因だと思われます。

セキュリティ対策の基本の1つとして「多重のセキュリティ対策」があります。これはセキュリティ対策の原則とも言われている重要な考え方です。セキュアなコードでは入力でバリデーションした値であっても出力する際には必ずエスケープを行います。しかし、これはコーディングのベストプラクティスである「重複処理を排除した効率よいコード記述」に反します。このため、既にチェック済み、これは数値だからエスケープ処理は不要、などとの誤った判断からエスケープ処理を行っていないコードで溢れています。

セキュリティ対策とコーディングのベストプラクティスは対立する考え方ですが、セキュリティ対策が必要な部分ではコーディングのベストプラクティスは忘れて、セキュリティ対策のベストプラクティスを取り入れるべきです。

よいコードとは次のようなコードです。

  • 必要なセキュリティ対策コードを書いた上で、重複などを排除した効率がよいコード

よいコードの必要条件は「必要なセキュリティ対策コードが含まれてること」です。この考え方を持たないプログラマが冒頭で紹介したような脆弱なコードを書いているのではないかと考えています。

セキュリティ対策の基本中の基本

セキュリティ対策の基本中の基本はどこでセキュリティ対策を行うかです。これが理解されていないケースも多くあります。プログラマから見たセキュリティ対策を行うべき箇所は多くありません。多くないどころかたったの3つです。

  • 入力(入力をバリデーション)
  • ロジック
  • 出力(出力をエスケープ、バリデーション)

この3つしかありません。この3つに分けて考えてセキュリティ対策を行えば、どのような対策をすべきであるか、初心者プログラマであっても間違える可能性はかなり低くなります。

入力

入力のチェックは必ず入力を受け入れた直後に、すべての入力パラメータに対して行います。これを怠るとセキュリティ上の問題の原因になります。入力のチェック(バリデーション)は必ず「すべての入力パラメータが正しいかチェックする」ようにします。入力チェックの基本もたった3つです。

  1. 期待している文字で構成されているか?
  2. 期待している範囲(長さ、値)のパラメータか?
  3. 期待している形式のパラメータか?

入力チェックのポイントは正しいものだけを受け入れることです。悪いもの(不正な文字など)を除去するブラックリスト的な考え方ではなく、正しいものだけを受け入れるホワイトリストの方針でチェックしなければなりません。別の言い方をすると「サニタイズはせず、バリデーションをする」ようにします。

広く利用されているアプリケーションやフレームワークだからといってベストプラクティスが採用されているわけではありません。例えば、Railsの入力のセキュリティ対策はセキュアであるとは言えません。Railsのバリデーションは「データベースにデータが保存される前」に行われます。データベースにデータを保存する必要がないようなアプリケーションの場合、入力のバリデーションをフレームワークとして行う仕組みになっていません。本来入力はデータベース利用の有無に関わらず入力を受け入れた直後に行うべきです。多くのフレームワークがRailsの影響を受け同様の仕様となっています。Railsが脆弱な仕様を採用したことは不幸なことだったと思います。

広く利用されているアプリケーションでも確実なセキュリティ対策が行われているわけではない、と理解しながら参考にしたり利用しなければなりません。

ロジック

ロジックの部分は認証(ログイン・ログアウト処理)や排他制御、フォームの正当性確認(CSRF対策など⁠⁠、アクセス制御などです。これらのロジックはそれぞれベストプラクティスがあります。フレームワークで実装されている場合も多くあります。初心者プログラマであってもこれらがしっかりしたフレームワークを利用していれば不適切な処理をしてしまうことは少なくなるでしょう。

言語や関数の仕様を知ることもロジックの部分に入ります。言語や関数の仕様を知らずに正しいロジックが書けるはずがありません。もし自分が利用している正規表現関数の仕様を調べたことがない方がいらしたら、ぜひ一度調べてみてください。もしかすると思わぬ発見ができるかも知れません。少なくとも入力、出力、バリデーションに関係する関数の仕様は確実に把握するようにしなければなりません。

入力処理や出力処理もアプリケーション処理のロジックと言えます。入力と出力は多重のセキュリティを取り入れる部分なので独立した処理として作ります。入出力処理はセキュリティ対策の要であり9割のアプケーション脆弱性は入出力にある、と思ってよいでしょう。セキュアなアプリケーションを構築するためには、入出力とほかのロジックとは別に独立したセキュリティ対策を行う箇所であると考えたほうが安全なコードを書けるようになります。

出力

「出力のエスケープ」は脆弱性を作らないという観点からは最も重要な対策ですが、見落とされていることが多い部分です。これにはセキュリティ対策を考慮していないコーディングのベストプラクティス以外に、誤ったセキュリティ対策が完全である、との思い込みも含まれているからだと考えています。出力する場合は「出力先のシステムが誤作動しないように正しい出力を行う」ようにします。出力処理を行う場合の基本的なセキュリティ対策もたった3つです。

  1. 出力先のシステムが誤作動しないようにすべてのパラメータをエスケープして出力する
  2. 出力先のシステム用にヘルパー関数などがある場合、ヘルパー関数の仕様を理解して利用する
  3. 1と2の対策が不可能な場合は出力時にバリデーション処理を行う

1で重要なのは「多重のセキュリティ対策」の原則です。入力処理と出力処理でセキュリティ対策が重複していても「必ず」エスケープして出力しなければなりません。

2は非常に誤解が多い部分です。プリペアードクエリさえ利用していれば安全、Viewのヘルパーさえ利用していれば安全、などと間違った思い込みから脆弱なアプリケーションを作ってしまう可能性を排除できていない開発者が多くいます。データベース管理用のアプリケーションでなくてもユーザ入力を使ったテーブルやフィールドの操作が必要となる場合もあります。例えば、アプリケーションのインストーラで必要となることは多いです。この場合、プリペアードクエリであっても識別子(テーブル、フィールド名)はパラメータになります。基本1の通りエスケープするか1)3のバリデーションを行わなければなりません。

Viewヘルパーを利用すれば安全、と考えるのは危険です。Railsアプリケーションのソースコード検査も仕事でしていますが、Railsで開発している方でもViewヘルパーがすべてのパラメータをエスケープ処理してくれないことを知りません。これはRailsだけの問題でなくすべてのWebアプリケーションフレームワーク共通の問題だと思ってください。

プリペアードクエリもViewヘルパーも、あくまで出力の基本1のエスケープ処理を補助してくれる機能に過ぎません。プリペアードクエリは元々 SQL実行を効率化する仕組みであり、完全なセキュリティ対策を行う仕組みとして設計されていません。クエリヘルパーだ、くらいに思っているほうがよいでしょう。

最後にパラメータであってもエスケープもできず、安全なヘルパーも使えない場合は3で記述したようにバリデーションするしかありません。ここに書いた通りにすべての入力をバリデーションしていればそのまま出力しても安全なはずですが、出力時にもバリデーションを必ず行うのが「多重のセキュリティの原則」です。

「出力先のシステムが誤作動しないように正しい出力を行う」と心掛けることは重要です。例えば、ここで解説した3の出力セキュリティ対策には正しいHTTPレスポンスを送る場合に必要な情報がありません。しかし、⁠出力先のシステムが誤作動しないように正しい出力を行う」を心掛けていればHTTPレスポンスで文字エンコーディン指定も含めた正しいコンテントタイプを設定しなければならない、という決まりごとに気付くことができます。

「出力先のシステムが誤作動しないように正しい出力」とは何か?把握した上で出力処理を行えば安全な出力を行うコードを書けるようになります。

誤解が無いように書いておきます。この記事はセキュリティ対策を主題に書いています。エスケープ機能を使い、ヘルパー機能を使うな、という意図で書いているわけではありません。セキュリティ対策の基本と一般的によいとされるコードの書き方では重要視すべき順序が異なるだけです。出力では「出力先のシステムが誤作動しないように出力する」のが安全な出力の基本であり、エスケープ処理がその柱です。

Viewヘルパーなどの機能は出力先のシステムが誤作動しないように補助する機能で不完全なことも多くあります。ヘルパー機能がエスケープ処理なしに正しく出力できるか?プログラマは理解してから使う必要があります。安全な出力を行えるかチェックするためにも、プログラマはまずエスケープ処理を正しく理解していなければなりません。

まとめ

特にプログラミングをはじめたばかりの方は、効率がよいコードを書くことに注力し過ぎているのではないでしょうか? セキュリティと効率は相反する部分があると理解し、効率化のためにセキュリティ対策を怠るのは悪いプラクティスである、と理解すればよりよいコードが書けるようになると考えています。

  • セキュリティ対策とコーディングのベストプラクティスは相反することを理解する
  • セキュリティ対策の基本中の基本を理解する

少なくともこの2つを理解して開発すれば、初心者であっても基本的な部分で脆弱性があるコードを書いてしまう可能性が低くなるでしょう。この2つを理解した上で、個々のセキュリティ対策を理解していけば自然に安全なアプリケーションが作れるようになると思います。

最後に「何が正しいのか?」常に考えるようにしてください。一見、正しいように見えても実は間違っている場合もあります。例えば、SQL文を作成する場合にリテラル(パラメータ)を文字列としてエスケープすると浮動小数点型のデータが正しく処理されないデータベースがあります。これは、SQLインジェクション対策の基本の1つである「すべてのリテラルを文字列として処理」することが間違っているのでしょうか? それとも「文字列として渡された浮動小数点リテラルを正しく処理できない」処理系が間違っているのでしょうか?

データベースはデータを安全かつ確実に保存するためのアプリケーションです。浮動小数点データがSQL文のトークンとして渡された場合でも文字リテラルとして渡された場合でも「データを安全かつ確実に保存すべきデータベース」には正しく処理できることが求められます。本来はSQLパーサでパースしても、文字リテラルパーサでパースしても、同じ結果となって当然です。つまり、浮動小数点文字リテラルが正しく処理できない処理系が間違っている、と考えるほうが筋が通っています。

このようなシステムに対応するための知識は実務的なノウハウとしては必要かつ重要ですが、正しいセキュリティ対策の指針を変更したりするようなことではありません。あるエスケープ関数にバグがあるからエスケープ関数は利用せず独自にエスケープする、と同類の、必要な知識だけれども広く適用すべきではないバッドノウハウと言えます。

今回はセキュリティ対策の基本中の基本だけれども、何故か確実に実施されていない理由と基本的な考え方を紹介しました。

おすすめ記事

記事・ニュース一覧