Goにおける型によってSQLインジェクションを防ぐ方法
はじめに
2022年のセキュリティ・キャンプ全国大会に講師として参加しました。その際に、Goにおける脆弱性への対策はどうなっているのか調べました。この記事では、github.com/google/go-safeweb/safesqlがどのようにSQLインジェクションを防いでるのかについて解説します。
なお、@rungさんの文書を多いに参考にしております。また、セキュリティ・キャンプで用いた資料はこちらから閲覧できます。
SQLインジェクションとは?
独立行政法人情報処理推進機構(IPA)が公開している安全なウェブサイトの作り方を見ると、SQLインジェクションは以下のように説明されています。
データベースと連携したウェブアプリケーションの多くは、利用者からの入力情報を基にSQL文(データベースへの命令文)を組み立てています。ここで、SQL文の組み立て方法に問題がある場合、攻撃によってデータベースの不正利用をまねく可能性があります。このような問題を「SQLインジェクションの脆弱性」と呼び、問題を悪用した攻撃を、「SQLインジェクション攻撃」と呼びます。
問題のあるSQL文の組み立て方法とは、プレースホルダーを用いずに文字列結合などでSQL文を構成することです。たとえば、次のようなSQL文の組み立て方はSQLインジェクションの対象となり得ます。
query := "SELECT * FROM users WHERE user_id = '" + req.FormValue("user_id") + "'"
req.FormValue("user_id")
は、*http.Request
型のFormValue
メソッドの呼び出しであり、リクエストから取得した文字列になります。そのため、"1' OR '1' = '1"
のような値が送られてくるとすべてのユーザの情報が取得できてしまいます。
そのため、問題のある文字列結合を避ける方法が必要となり、静的解析ツールなどが作成されています。
go-safeweb
go-safewebは、GoogleがOSSとして公開しているセキュアプログラミングを行うためのライブラリです。go-safewebはGoogle公式の製品ではないことがREADMEに記載してあります。
go-safewebは、secure-by-defaultを掲げており、セキュアな状態をデフォルトとしたnet/httpパッケージやdatabase/sqlパッケージの代替になるパッケージを公開しています。
safesqlとSQLインジェクションを防ぐしくみ
go-safewebにある、safesqlはdatabase/sqlパッケージのラッパーのようなパッケージです。しかし、SQLインジェクションがなるべく起きないように設計されています。
たとえば、(*sql.DB).QueryContextに該当する(*safesql.DB).QueryContextが存在します。シグネチャはほとんど同じように見えますが、第2引数のクエリ文字列がsafesqlの場合は、TrustedSQLString
型になっています。
TrustedSQLString
型は次のように定義されている構造体です。
type TrustedSQLString struct {
s string
}
単純に文字列をラップしただけのように見えます。しかし、フィールドs
をエクスポートしていないため、TrustedSQLString
型の値を生成する方法は限られています。たとえば、構造体リテラルですが、公開されていないフィールドを指定して初期化することはできません。また、フィールドを省略した構造体リテラルも生成できませんし、new
関数を使ってオブジェクトを生成し、フィールドに設定することもできません。
// NG: フィールドを指定できない
str1 := safesql.TrustedSQLString{s: "SELECT * FROM users"}
fmt.Println(str1)
// NG: フィールドを省略指定できない
str2 := safesql.TrustedSQLString{"SELECT * FROM users"}
fmt.Println(str2)
str3 := new(safesql.TrustedSQLString)
// NG: フィールドに代入できない
*str3.s = "SELECT * FROM users"
fmt.Println(*str3)
TrustedSQLString
型の値を生成するには、safesql
パッケージで公開されている関数を使用する必要があります。safesql.New関数はTrustedSQLString
型の値を生成する関数の1つで文字列を受け取ります。New
関数は次のように定義されています。
func New(text stringConstant) TrustedSQLString {
return TrustedSQLString{string(text)}
}
受け取った文字列を単にs
フィールドに設定しているように見えますが、stringConstant
型という見慣れない型として引数を受け取っています。stringConstant
型はsafesqlパッケージで次のように定義されています。
type stringConstant string
string
型をUnderlying Typeとして持つ文字列型になっています。なお、Underlying Typeについては、@DQNEOさんの入門Go言語仕様 Underlying Typeが参考になります。
stringConstant
型とstring
型は同じUnderlying Typeを持つ[1]ため、相互で型変換(キャスト)が可能です。
New
関数は、次のように文字列リテラルを指定してTrustedSQLString
型の値を生成できます。
str := safesql.New("SELECT * FROM users")
fmt.Println(str)
引数に文字列リテラルを渡すことができるのであれば、次のように文字列結合を使ったSQLインジェクションが可能のように思えます。
// 本来は初期化されている
var db *safesql.DB
func handler(w http.ResponseWriter, r *http.Request) {
userID := r.FormValue("user_id")
str := safesql.New("SELECT * FROM users WHERE user_id = '" + userID + "'")
ctx := context.Background()
rows, err := db.QueryContext(ctx, str)
// 略
_, _ = rows, err
}
しかし、実際にコンパイルしようとするとコンパイルエラーになります。stringConstant
型が必要なところにstring
型の値が指定されているというエラーです。つまり、引数の型が誤っているため発生するエラーです。
たしかに、New
関数の引数はstringConstant
型であるため、次のように型変換(キャスト)すれば良さそうですが、できません。stringConstant
型が公開されていないからです[2]。
s := "SELECT * FROM users WHERE user_id = '" + userID + "'"
str := safesql.New(safesql.stringConstant(s))
それでは、なぜ文字列リテラルはNew
関数に渡すことができたのでしょうか?Goには、型なしの定数というものがあり、型なしの定数は明示的な型変換なしで型を持つ定数や変数と演算ができます。もちろん、文字列と数値などの変換ができない型もありますが、この性質は非常に便利です。よく見かける例として、time.Duration
型の値があるでしょう。たとえば、5秒を表すtime.Duration
型の値は、次のように書きます。
const timeout = 5 * time.Second
time.Second
定数はtime.Duration
型である型ありの定数ですが、5
はそうではありません。型を持たない定数(数値リテラル)となっています。型を持たない定数と型を持つ定数(または変数)の演算は明示的な型変換が不要なため、5 * time.Second
と書けるようになっています。もし、型なしの定数がなければ、time.Duration(5) * time.Second
と書く必要があり、面倒です。このあたりの設計の話はGo Conference 2014 AutumnでRob Pike氏がキーノートで話されていましたが、気になる方はこちらからリンクを辿ると良いでしょう。
話をsafesqlに戻すと、New
関数の引数は文字列リテラルまたは型なしの文字列になる定数式のみが許されます。つまり、変数は必ず型を持つため、変数と演算(文字列結合)してしまうと、string
型[3]になってしまいます。言い換えると、New
関数の引数はコンパイル時に決定しているような定数しか許されず、実行時にならないと分からない、リクエストを起因とするデータは扱えないということになります。
そのため、クエリに値を埋め込みたい場合はクエリ文字列に?
などのプレースホルダを指定し、(*safesql.DB).QueryContext
メソッドの第3引数以降で指定する必要があります。
おわりに
本記事では、safesqlパッケージがどのようにSQLインジェクションを防いでいるか解説しました。脆弱性を見つけるには、動的解析や静的解析などのプログラム解析によって防ぐこともできますが、完全ではありません。型システムで防ぐとより安全で正確に防げます。Goでは型システムによって、このような脆弱性を防ぐことは、そう多くはありませんが、選択肢の1つとして知っておくことは良いことでしょう。
secure-by-default
の考え方は非常に勉強になるので、ぜひgo-safeweb
についても調べてみると良いでしょう。
Discussion