🛡️

Goにおける型によってSQLインジェクションを防ぐ方法

2022/08/12に公開約5,700字

はじめに

2022年のセキュリティ・キャンプ全国大会講師として参加しました。その際に、Goにおける脆弱性への対策はどうなっているのか調べました。この記事では、github.com/google/go-safeweb/safesqlがどのようにSQLインジェクションを防いでるのかについて解説します。

なお、@rungさん文書を多いに参考にしております。また、セキュリティ・キャンプで用いた資料はこちらから閲覧できます。

SQLインジェクションとは?

独立行政法人情報処理推進機構(IPA)が公開している安全なウェブサイトの作り方を見ると、SQLインジェクションは以下のように説明されています。

データベースと連携したウェブアプリケーションの多くは、利用者からの入力情報を基にSQL文(データベースへの命令文)を組み立てています。ここで、SQL文の組み立て方法に問題がある場合、攻撃によってデータベースの不正利用をまねく可能性があります。このような問題を「SQLインジェクションの脆弱性」と呼び、問題を悪用した攻撃を、「SQLインジェクション攻撃」と呼びます。

https://www.ipa.go.jp/security/vuln/websecurity-HTML-1_1.html

問題のある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)

Go Playgroundで動かす

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)

Go Playgroundで動かす

引数に文字列リテラルを渡すことができるのであれば、次のように文字列結合を使った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
}

Go Playgroundで動かす

しかし、実際にコンパイルしようとするとコンパイルエラーになります。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についても調べてみると良いでしょう。

脚注
  1. ともにstring型をUnderlying Typeとして持つ ↩︎

  2. Goでは最初の文字が小文字から始まる識別子は別のパッケージから参照できません ↩︎

  3. またはstring型をUnderlying Typeに持つ型でそのパッケージで参照可能な型(stringConstant型は含まない ↩︎

Discussion

ログインするとコメントできます