セキュアなプログラミングには基本的な考え方があります。それ守ることによりセキュアなプログラムを作ることができます。基本的な考え方を無視または意識しないでセキュアなプログラミングを目指しても遠回りだったり、漏れが生まれたりします。基本的な考え方を無視・意識しないでセキュアなプログラミングを行おうとしても無理があります。
ここで紹介するのは私の考えであり、どこかで標準化されている物ではありません。しかし、基礎からまとめると概ね同じような物になると思います。
セキュアプログラミングの7つの習慣
あまり数が多過ぎても覚えきれないのでマジックナンバーの7つに限定してみます。(「7つの習慣」がお気に入りであることも理由です)習慣化することにより自然により安全なプログラムを書けるようになると思います。可変長テキストインターフェースを用いるWebアプリケーションのようなプログラムを書くプログラマーを想定しています。固定長・バイナリインターフェースを用いるプログラムの場合は多少異る物になります。
1. ゼロトラストを実践する
ゼロトラスト(Zero Trust)とは言葉の通り、何も信頼しないことを意味します。何かを信頼するには信頼に足る根拠が必要です。安全なプログラムを書くには「根拠がない物は全て信頼しない」という考え方が必要なります。何かを信頼するには必ず「根拠」が必要であり、論理的に根拠を示すことが可能でなければなりません。
ゼロトラストと似た言葉にゼロトーレランス(Zero Tolerance)がありますが、これは全く異なる概念です。ゼロトーレランスとはいかなるセキュリティ脆弱性も許容しない、という考え方です。システム運用などで既知の脆弱性修正は全て適用するなどの方針、プログラムの入力バリデーション方針/出力のエスケープ処理方針などに利用されます。
プログラミングで信頼できるモノは「自分が書いたコードのみ」になります。自分が書いたコードも信頼できない?!普通はそうです。しかし、もしかしたら悪意を持って書いたかもしれない他人のコードよりは信頼できると思います。誰が作ったり、送ったりしたのか分らないデータも信頼できません。プログラムの中にハードコードされているデータだけが信頼できるデータです。例えば、ネットワークやOSのファイルシステムを経由したデータは全て信頼できません。
根拠無く自分が書いたコード以外のモノを信頼してはなりません。
信頼できる、とされている熟れたライブラリやフレームワークも盲信してはなりません。こういったコードにもバグはあります。自分のコードで可能なセキュリティ対策は自分で実施しなければなりません。
最後に自分が書いたコードも信頼しないようにすることも必要です。例えば、入力処理時にデータが英数字のみとバリデーションしていても、出力処理時には全てエスケープするか、エスケープが必要ないAPIを使用するか、バリデーションします。
2. 信頼境界線を意識する
ゼロトラストは根拠なく何も信頼しない考え方です。逆に言うと「根拠があれば信頼する」ことを意味します。根拠があり信頼できるモノ、根拠がなく信頼できないモノ、その境界が信頼の境界線になります。
何の信頼もなくプログラムを構築することはできません。例えば、ブラウザが正しいHTMLマークアップを正しく解釈する、RDBMSが正しいSQL文を正しく解釈する、といった信頼はアプリケーション構築に欠かせません。これらは自分が制御できない外部システムが仕様通りに動作することを信頼(期待)することを意味します。自分が制御するローカルのプログラム以外は基本的に全て信頼境界の”外”になります。(つまりこれらの入出力は信頼できない)
問題のない信頼境界線を書くには「ゼロトラストの実践」が欠かせません。何かを信頼境界線の中に入れるには「信頼にたる根拠」が必要です。根拠が十分であれば信用することができますが、その根拠とする考え方を間違えてしまうことも多いです。例えば、「このファイルは自分のプログラムで書いたもの、書くものだから信頼できる」と考えてしまうと不十分です。そのファイルは攻撃者が作ったファイルやシンボリックリンクかも知れません。
様々なデータが信頼できるモノとして扱えるようにする方が、プログラミングが楽です。このため外部システムであっても信頼できるモノとして取り扱えるようにしたり、データをバリデーションしたりします。(参考:契約プログラミング)信頼境界線を意識していても間違えてしまうことが多いです。意識していなければ間違えてしまうのは当たり前でしょう。
3. 入力の正しさを検証する
信頼境界線を越えてプログラムに入ってくる入力が正しい物であるか、必ず検証しなければなりません。入力が「形式的に正しい」か検証することは比較的簡単です。入力データの形式/大きさなどを検証すれば良いです。自分の作っているプログラムの入力データの形式や大きさが全く見当も付かないようならプログラムを書く前に仕様を考える必要があります。書いているプログラムの仕様が決まっていないため入力データの形式や大きさが判らない場合は、後で検証コードを追加しても構いません。
入力が「真正である」こと検証するのは比較的難しいです。「真正である」かどうかを考えるには”1. ゼロトラストの実践する”と”2. 信頼境界線を意識する”が必要です。ある入力・データが「真正」であるかどうかを判断するのはセキュリティ専門家でも困難であるケースが多くあります。今までに多くの暗号アルゴリズムや認証アルゴリズムのセキュリティ問題が見つかっていますが、これらの原因が入力・データの真正性検証であった物が多数あります。
真正性はセキュリティ対策の基本要素(信頼性、完全性、可用性、真正性、責任追跡性、否認防止、信頼性)の一つです。ISO27000の定義では
- 真正性 (authenticity): ある主体又は資源が、主張どおりであることを確実にする特性。真正性は、利用者、プロセス、システム、情報などのエンティティに対して適用する。
とされています。要するに、ある利用者、プロセス、システム、情報(入力・データ)が主張(期待)通りに、本来権限を持っているモノから送られたモノ、作られたモノ、であることを検証し確実にした状態が「真正性を維持した状態」と言います。
真正性を確保するのは困難を伴う場合もありますが、「これは本当に権限を持ったモノが送った(作った)モノなのか?」とゼロトラストを実践、信頼境界線を意識していれば自分で問題に気付く事も多いと思います。
真正性の評価/検証は困難な場合も多いですが、入力データの形式や大きさの検証は簡単です。少なくともこれだけは確実に押さえたいです。
4. 出力の正しさを確実にする
信頼境界線を超えて出力するデータが正しいモノであること確実にしなければなりません。コンピュータの制御を奪い攻撃者の命令を実行させるタイプの攻撃は「信頼境界線を超えて出力された命令・識別子・データに攻撃者の命令を紛れ込ませる」ことにより実現されます。Cプログラムで問題になるバッファーオーバーフロー攻撃は「メモリに出力されたデータの中に命令を紛れ込ませる」こと、SQL文で問題になるSQLインジェクション攻撃も「SQLに出力されたSQL文(DBの入力データ)に命令を紛れ込ませる」ことにより実現します。「出力の正しさを確実」にすれば、ほとんどの任意命令実行型の攻撃を防ぐことができます。
セキュアなSQL文の実行方法としてプリペアードクエリクエリの利用が推奨されています。しかし、プリペアードクエリはデータと命令・識別子を分離して、安全なデータの受け渡しを実現しますが命令・識別子を安全に分離することはできません。この為、「SQL文の実行にプリペアードクエリを使う」だけでは「出力の正しさを確実」にすることはできません。最近のソースコード検査では、ある程度セキュリティ意識の高い組織の作ったコードの場合はデータ(パラメータ)によるSQLインジェクション脆弱性はほとんど見つからず、見つかるSQLインジェクションに脆弱なコードの多くは識別子のエスケープ漏れ/バリデーション漏れによるモノです。セキュリティ問題となるケースは多くありませんがLIKE句のクエリ表現のエスケープがないケースは未だに多くあります。LIKEクエリでそのユーザーが選択できるデータを制限している場合、エスケープがないと情報漏洩につながる場合もあります。
セキュアコーディングの基本として「安全なAPIを使う」は正しいですが、「安全なAPIであることを確実にする」「安全なAPIの制限を理解(検証)する」は欠かせない考え方です。例えば、Railsでメールを送信する場合、最終的にメールを送信するコードは利用するモジュールによって決まります。モジュールが無効な入力や危険な入力を許容している場合、不正にメールが送信される・メッセージが意図しない形で送信される(改ざんされる)可能性があります。
出力の正しさを確実にするは、テキスト型のインターフェースなら安全なテキスト処理の基本を理解しておく必要があります。基本を押さえていれば、どのようなテキストインターフェースでも出力の正しさを確実にすることが可能です。
出力時のみの処理で「出力の正しさを確実にする」のは困難や無駄を伴う場合も多いです。「入力の正しさを検証する」と合わせて考える必要があります。しかし、最終的に外部のシステム(ブラウザやDBMS)に出力する際には”無駄”も必須です。例えば、SQLの識別子は「英数字のみでバリデーション済みなのでエスケープしない」ではなく「変数であれば全て例外なくエスケープする」とすれば正しい出力であることを確実にできます。
5. 予想外のエラーで実行を停止する
例えば、プログラムが意図しない不正な入力を受け入れて処理することに意味はありません。不正な入力を受け入れると無用なセキュリティリスクを増やすだけです。入力データの形式/大きさなどがあり得ないモノであった場合、普通は即座に処理を停止すべきです。(例外は壊れたデータのリカバリツールなど)
システムで想定していないエラーが発生した場合、リカバリーを試みてはなりません。想定外のエラーなどが発生した場合、プログラムの実行を停止させます。攻撃者はプログラマが想定しない入力をシステムに送りつけたり、想定しない状態を作って不正なコード・命令を実行させようとします。
対話型のシステムからの入力、例えばWebシステムの入力フォームでのユーザー入力ミスは「想定外の入力」ではありません。JavaScriptなどで入力フォームの形式をチェックしていない場合、入力ミスは想定内の入力です。間違えないようにしてください。
予想外のエラーで確実に処理が停止するようなコードになる(エラーを無視しないなど)よう注意しましょう。
6. システムのイベントを記録する
システムで発生したイベントは可能な限り詳しく記録します。認証、特別な権限の使用、セキュリティ設定の変更やエラーなどの重要なイベントは必ず記録します。データ操作や参照も重要なモノは記録します。重要でないモノもできる限り記録します。
これらのイベントを記録する場合には、イベント発生の時刻はもちろん、できる限りユーザーを特定する情報も一緒に記録します。WebアプリならユーザーIDのみでなくクライアントのIPアドレス、ユーザーエージェント、セッションIDも記録します。セッションIDを保存してもセッションデータは自動削除されるので意味がない、と思うかも知れませんがイベント発生直後ならセッションデータを確認できる場合もあります。
システムのイベントを記録しても攻撃は防げませんが、責任追跡性や否認防止は基本的なセキュリティ要素であり、重要なセキュリティ対策です。最低限でも認証やセキュリティ設定の変更などの重要なイベントは記録しましょう。
7. 秘密であるべき情報を秘密にする
秘密であるべき情報を秘密にすることは、当たり前のことのように思えますがこれができていないため問題にとなるケースも少なくありません。システムの内部情報を含むエラーメッセージ、認証エラーの場合にユーザー名とパスワードのどちらが間違っているか、などは「秘密にすべき情報」です。
配布するプログラムの中にユーザー名とパスワードをハードコードしていた、という間違いも結構な数であります。たとえ分りづらくしていたとしても「秘密であるべき情報」を配ってしまうと解読される可能性があります。配布しないプログラムであっても、gitなどのソースコードリポジトリに保存してしまうと、見てはならない人が見てしまう可能性があります。
秘密であるべき情報の機密性が十分に保たれるように意識しましょう。
まとめ
- ゼロトラストを実践する
- 信頼境界線を意識する
- 入力の正しさを検証する
- 出力の正しさを確実にする
- 想定外のエラーで停止する
- システムのイベントを記録する
- 秘密であるべき情報を秘密にする
多くはコンピューターの動作原理(セキュアコーディングの原理・原則)から導き出せるベストプラクティスです。
他にもセキュアプログラミングに重要な基本的な考え方もありますが、まずはこれらを押さえると、これからプログラミングを初める方も既にバリバリ書いている方も、自分自身で安全なコードが書けるようになると思います!取り敢えず2016年版として公開します。
参考
- CERT Top 10 Secure Coding Practices
- エンジニア必須の概念 – 契約による設計と信頼境界線
- OWASP Secure Coding Practices – Quick Reference Guide
- 開発者は必修、SANS TOP 25の怪物的なセキュリティ対策