はじめに(Introduction)
Go言語でBigQueryを操作する際、ユーザー入力など動的な値を用いてクエリを構築することがあります。
その際、SQLインジェクション対策として最初は strings.ReplaceAll でエスケープ処理を試みましたが、「これでは不十分(微妙)だ」とレビューでフィードバックを受け、最終的にBigQueryクライアントライブラリ標準のクエリパラメータへ移行しました。
本記事では、なぜ自前のエスケープ処理が微妙だったのか、そしてクエリパラメータを使うことで何が改善されたのかを、5W1Hの視点で振り返ります。
1. 当初の対応:strings.ReplaceAll によるエスケープ
最初は、以下のように文字列置換を使って、「シングルクォート '」をエスケープする方法を実装していました。
😢微妙だったコード(Before)
package main
import (
"fmt"
"strings"
)
func buildQuery(userName string) string {
// ユーザー入力に含まれるシングルクォートをエスケープ(' -> \')
// ※ SQLインジェクション対策のつもり
safeName := strings.ReplaceAll(userName, "'", "\\'")
// fmt.SprintfでSQLを組み立てる
results := fmt.Sprintf("
SELECT
*
FROM
`my-project.dataset.users`
WHERE
name = '%s'", safeName)
return results
}
⚠️必要最低限のソースコードになってます。
なぜこれが「微妙」だったのか
一見良さそうに見えますが、以下の点で不安要素(微妙な点)が残りました。
- ヌケモレの不安(Security): シングルクォート以外(バックスラッシュなど)のエッジケースを完全に網羅できているか自信が持てない。「自前実装」はセキュリティホールの温床になりやすい。
-
可読性の低下(Readability):
fmt.Sprintfとエスケープ処理が混在し、SQLの構造が見えにくくなる。 -
型安全ではない(Type Safety): 全てを
stringとして扱うため、日付型(TIMESTAMP)や数値型を扱う際にフォーマット変換の手間が発生する。
2. 改善後の対応:Query Parameters の使用
そこで、Google Cloud 公式のGoクライアントライブラリ(cloud.google.com/go/bigquery)が提供している Query Parameters(パラメータ化クエリ) を使用する方法に切り替えました。
😆 良かったコード(After)
package main
import (
"context"
"cloud.google.com/go/bigquery"
)
func runQuery(ctx context.Context, client *bigquery.Client, userName string) error {
// プレースホルダ(@name)を使用したクエリ
q := client.Query("
SELECT
*
FROM
`my-project.dataset.users`
WHERE
name = @name")
// パラメータの設定
q.Parameters = []bigquery.QueryParameter{
{
Name: "name",
Value: userName, // エスケープ処理はライブラリに任せる
},
}
it, err := q.Read(ctx)
if err != nil {
return err
}
// ... 処理の続き
return nil
}
3. 何が良かったのか(5W1Hで整理)
今回の移行で感じたメリットを、5W1Hのフレームワークで整理しました。
| 5W1H | 内容 | Before (ReplaceAll) | After (Query Parameters) |
|---|---|---|---|
| Who | 誰が対策するか |
自分(開発者) 自前で置換処理を書く責任がある。 |
ライブラリ(Google) 公式SDKが安全に値を処理してくれる。 |
| What | 何を防ぐか | 文字列置換で特定の文字だけを無効化。 | クエリ構造とデータを分離することで、SQLインジェクションそのものを無効化。 |
| When | いつ処理されるか | SQL文字列を生成する時点。 | クエリが実行エンジンに渡される時点(またはその直前)。 |
| Where | どこで定義するか | コード内の文字列操作関数の中。 |
bigquery.QueryParameter 構造体の中。 |
| Why | なぜ安全か | 「危険な文字を消したから安全」という仮定に基づく。 | 「コード」と「データ」が明確に区別されるため、データがコードとして解釈される余地がない。 |
| How | どのように書くか |
fmt.Sprintf で文字列結合。 |
SQL内で @param を使い、Goの型をそのまま渡す。 |
特に「良かった」ポイント
- 責務の分離: 「SQLの構文」と「ユーザー入力値」が明確に分かれるため、コードが直感的になりました。
-
Goの型システムの活用: Goの
time.TimeなどをそのままValueに渡せば、BigQuery側のTIMESTAMP型などに適切にマッピングしてくれます。文字列変換の手間が省けました。
まとめ
strings.ReplaceAll によるエスケープは、手軽に実装できる反面、セキュリティの担保を開発者自身が背負うことになり、複雑な攻撃パターンに対して脆弱になりがち(=微妙)です。
BigQuery Clientを使用する場合は、提供されている Query Parameters 機能を使用することで、「より安全に(Security)」「より読みやすく(Readability)」「Goらしく(Type Safety)」 実装することができます。
私と同じ轍(てつ)は踏まないでくださいね