ユーザのデータをフィルタリングするのはWebアプリケーションセキュリティの基礎です。これはデータの合法性を検証するプロセスです。すべての入力データに対してフィルタリングを行うことで、悪意あるデータがプログラム中に誤って信用され使用されるのを防ぐことができます。大部分のWebアプリケーションのセキュリティホールはどれもユーザが入力したデータに対して適切なフィルタを行わなかったことによるものです。
我々がご紹介するデータのフィルタリングは3つのステップに分かれています:
- 1、データを識別し、フィルタリングすべきデータがどこから来たのかはっきりさせる
- 2、データをフィルタリングし、どのようなデータが必要なのか明らかにする
- 3、フィルタリングされ汚染されたデータを区別し、もし攻撃データが存在する場合はフィルタリング後我々がより安全なデータを使用するよう保証する
"データの識別"の第一歩では"データが何か、どこから来たのか"を知らないという前提があるため、これを正確にフィルタリングすることができません。ここでいうデータとはコードの内部以外から提供されているすべてのデータを指します。例えば:すべてのクライアントからのデータ、ただしクライアントだけが唯一の外部データ元ということではありません。データベースと第三者が提供するインターフェースデータ等も外部データ元となりえます。
ユーザが入力したデータはGoを使って非常に簡単に識別することができます。GoはrParseForm
を使って、ユーザのPOSTとGETのデータをすべてr.Form
の中に保存します。その他の入力は識別するのがとてもむずかしくなります。例えばr.Header
の中の多くの要素はクライアントが操作しています。この中のどの要素が入力となっているかは確認するのが難しく、そのため最良の方法は中のすべてのデータをユーザの入力とみなしてしまうことです。(例えばr.Header.Get("Accept-Charset")
といったものも大多数はブラウザが操作しているものの、ユーザの入力とみなします。)
データの発生源を知っていれば、フィルタリングを行うことができます。フィルタリングというのは少し正式な専門用語で、普段使われる言葉では多くの同義語が存在します。たとえば検証、クリーニング、サニタイズといったものです。これらの専門用語は表面的な意味は異なりますが、いずれも同じ処理のことを指しています。望ましくないデータがあなたのアプリケーションに入ってくるのを防止します。
データのフィルタリングには多くの方法があります。そのうちいくつかは安全性に乏しく、最良の方法はフィルタリングを検査のプロセスとみなしてしまうことです。あなたがデータを使用する前に、合法的なデータに合致したリクエストであるか検査し、気前よく非合法なデータを糾弾しようとはせず、ユーザに規定のルールでデータを入力させることです。非合法なデータを糾弾することは往々にしてセキュリティ問題を引き起こすことを歴史が証明しています。例をあげましょう:"最近銀行システムのアップグレードがあった後、もしパスワードの後ろ二桁が0であった場合、前の四桁を入力するだけでシステムにログインできます"。これは非常に重大なセキュリティホールです。
データのフィルタリングは主に以下のようなライブラリを採用することで操作されます:
- strconvパッケージの文字列変換関連の関数。Requestの中の
r.Form
が返すのは文字列であり、時々これを整数または浮動小数点数に変換する必要がありますから、Atoi
、ParseBool
、ParseFloat
、ParseInt
といった関数を利用することができます。 - stringパッケージのいくつかのフィルタリング関数
Trim
、ToLower
、ToTitle
といった関数。我々が指定する形式にしたがってデータを取得することができます。 - regexpパッケージを使って複雑な要求を処理します。例えば入力がEmailかどうかや誕生日かどうかを判断します。
データのフィルタリングは検査や検証を除いて、特殊な場合ホワイトリストを採用することができます。つまり例えばあなたが今検査しているデータが合法であると証明されないかぎり、どれも非合法であったとします。この方法ではもしエラーが発生すると合法的なデータを非合法であるとするかもしれませんが、その逆はありません。どのようなエラーも犯さないと思っていても、このようにすることは非合法なデータを合法としてしまうよりもずっと安全です。
もし上の2ステップが完了すると、データフィルタリングの作業は基本的に完了です。しかしWebアプリケーションを書いている時我々はすでにフィルタリングして汚染されているデータを区別する必要があります。なぜならこのようにすることでデータのフィルタリングの完全性を保証し、入力したデータには影響を与えないようにすることができるからです。我々はすべてのフィルタリングされたデータをグローバルなMap変数(CleanMap)の中に保存します。この時2つの重要なステップで汚染されたデータが注入されるのを防ぐ必要があります:
- 各リクエストはCleamMapを空のMapとして初期化する必要があります。
- 検査を加えて外部のデータ元の変数がCleanMapとされるのを阻止する。
続けて、例を一つ挙げてこの概念を強固にしましょう。下のフォームをご覧ください
<form action="/whoami" method="POST">
あなたは誰ですか:
<select name="name">
<option value="astaxie">astaxie</option>
<option value="herry">herry</option>
<option value="marry">marry</option>
</select>
<input type="submit" />
</form>
このフォームのプログラムロジックを処理している時に、非常に簡単に犯してしまう間違いは3つの選択肢の一つだけが送信されると思い込んでしまうことです。攻撃者はPOST操作をいじることができますから、name=attack
といったデータを送信できます。そのため、この時ホワイトリストににた処理を行う必要があります。
r.ParseForm()
name := r.Form.Get("name")
CleanMap := make(map[string]interface{}, 0)
if name == "astaxie" || name == "herry" || name == "marry" {
CleanMap["name"] = name
}
上のコードではCleamMapという変数をひとつ初期化しています。取得したnameがastaxie
、herry
、marry`の3つの打ちの一つだと判断した後、データをCleanMapに保存します。このようにCleanMap["name"]のなかのデータが合法であると保証することができます。そのためコードの他の部分にもこれを使用します。当然else部分に非合法なデータの処理を追加してもかまいません。再度フォームを表示しエラーを表示するといったこともできます。しかしフレンドリーに汚染されたデータを出力してはいけません。
上の方法はすでに知っている合法な値のデータをフィルタリングするのには有効ですが、すでに合法な文字列で構成されていると知っているデータをフィルタリングする場合はなんの助けにもなりません。例えば、ユーザ名をアルファベットと数字のみから構成させたいとする場合です:
r.ParseForm()
username := r.Form.Get("username")
CleanMap := make(map[string]interface{}, 0)
if ok, _ := regexp.MatchString("^[a-zA-Z0-9].$", username); ok {
CleanMap["username"] = username
}
データのフィルタリングはWebセキュリティにおいて基礎となる作用です。多くのセキュリティ問題はデータのフィルタリングと検証を行わなかったことによるものです。例えば前の節のCSRF攻撃と以降に説明するXSS攻撃、SQLインジェクション等はどれも真面目にデータをフィルタリングしなかった事によって引き起こされます。そのため、この部分の内容は特に重視する必要があります。