背景
mongo-go-driver の使い方として次のような書き方がよくあります。
func searchUserByName(collection *mongo.Collection, name string) ([]User, error) { query := bson.M{"name": name} // ここ cur, err := collection.Find(context.Background(), query) if err != nil { return nil, err } defer cur.Close(context.Background()) var users []User for cur.Next(context.Background()) { var user User if err := cur.Decode(&user); err != nil { return nil, err } users = append(users, user) } return users, nil }
果たしてこれはNoSQLインジェクションに対してどうなのかを考えてみます。
環境
- MongoDB 8.0
- Go 1.23.5
- mongo-go-driver v1.17.2
前提知識
SQLインジェクションとは
SQLインジェクションは、攻撃者がアプリケーションのデータベースクエリを操作するために、悪意のあるSQLコードを入力フィールドに挿入する攻撃手法です。例えば、次のようなクエリがあるとします。
SELECT * FROM users WHERE username = 'admin' AND password = 'password';
攻撃者が admin' -- のような入力を行うと、クエリは次のように変わります。
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'password';
この結果、パスワードチェックが無効化され、攻撃者は管理者権限を取得することができます。
SQLインジェクション対策
通常はこういったSQLインジェクションを防ぐために、プリペアドステートメントを使用してユーザーの入力をそのままクエリに使用しないようにします。
stmt, err := db.Prepare("SELECT * FROM users WHERE username = ?") if err != nil { log.Fatal(err) } defer stmt.Close() rows, err := stmt.Query("admin") if err != nil { log.Fatal(err) } defer rows.Close()
NoSQLインジェクションとは
NoSQLも似たような物で、ユーザ入力をそのままクエリとして渡してしまうことにより、本来漏洩してはいけないデータを取得可能としてしまうような攻撃手法です。
const userInput = "{ $gt: '' }"; // 攻撃者が入力したデータと仮定 const query = { username: userInput }; db.collection('users').find(query).toArray((err, result) => { if (err) throw err; console.log(result); });
この例では、userInputに攻撃者が{ $gt: '' }のような入力を行うことで、すべてのユーザーが返される可能性があります。
mongo-go-driverの場合
このようにSQLであれNoSQLであれ、ユーザ入力をそのままクエリとして渡してしまうとNoSQLインジェクションの脆弱性が生まれる可能性があります。 ではmongo-go-driverではどうなのかを見ていきます。
問題無いケース
MongoDBの場合は、 bson.M や bson.D がプリペアドステートメントのような働きをしてくれるので、通常NoSQLインジェクションのような問題は発生しません。
// テスト用にサンプルデータを挿入 _ = collection.Drop(context.Background()) _, err = collection.InsertMany(context.Background(), []interface{}{ bson.M{"name": "Alice", "age": 25}, bson.M{"name": "Bob", "age": 30}, bson.M{"name": "Charlie", "age": 35}, }) if err != nil { log.Fatal("InsertMany error:", err) } userInputValue := 30 // 例えばフォームから受け取った数値 safeFilter := bson.M{ "age": bson.M{ // 演算子($gt)はアプリケーションが定義した値 "$gt": userInputValue, }, } fmt.Println("[Safe] filter:", safeFilter) cur, err := collection.Find(context.Background(), safeFilter) if err != nil { log.Fatal("[Safe] Find error:", err) } var safeResults []bson.M if err := cur.All(context.Background(), &safeResults); err != nil { log.Fatal("[Safe] Cursor error:", err) } fmt.Println("[Safe] Results (age > 30):", safeResults)
結果
該当するデータが返ってきます。
[Safe] filter: map[age:map[$gt:30]]
[Safe] Results (age > 30): [map[_id:ObjectID("678d575dfdb875f3cbd5abcc") age:35 name:Charlie]]
// => Charlie がヒット
演算子を入れてみる
これは仮に $gt のような演算子をいれても、内部では演算子で無く文字列として扱われるので、NoSQLインジェクションの脆弱性は発生しません。
userInputKeyword := `{"$gt": 30}` // 例えばフォームから受け取った文字列 justStringFilter := bson.M{ "age": userInputKeyword, // "$gt" はただの文字列 } fmt.Println("[JustString] filter:", justStringFilter) cur, err = collection.Find(context.Background(), justStringFilter) if err != nil { log.Fatal("[JustString] Find error:", err) } var justStringResults []bson.M if err := cur.All(context.Background(), &justStringResults); err != nil { log.Fatal("[JustString] Cursor error:", err) } fmt.Println("[JustString] Results:", justStringResults)
結果
[JustString] filter: map[age:{"$gt": 30}]
[JustString] Results: []
// => Charlie がヒットしない
気をつけるべきケース
ユーザーの入力をそのままクエリに使用する
しかし次のようにbson.Mをまるごとユーザーの入力として受け取ると、NoSQLインジェクションのような問題が発生します。
router.POST("/users", func(c *gin.Context) { var query bson.M if err := c.ShouldBindJSON(&query); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } collection := client.Database("test").Collection("users") result, err := collection.Find(c, query) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) })
ユーザ入力をキーとして使ってしまうケース
まるごと使わなかったとしても、クエリのキー部分にユーザー入力を許容してしまうとNoSQLインジェクションが可能になります。
userInputKey := "$gt" // ユーザー入力がキーとして入ってくると仮定 userInputVal := 30 unsafeFilter := bson.M{ userInputKey: userInputVal, } cur, err = collection.Find(context.Background(), unsafeFilter) if err != nil { log.Fatal("find error:", err) }
$whereのような自由度の高いクエリを許可してしまうケース
bson.Mでキーとして使っていなくても、 $where のような自由度の高いクエリを許可してしまうとNoSQLインジェクションの脆弱性が生まれる可能性があります。
userInput := "this.age > 30" filter := bson.M{"$where": userInput} collection.Find(context.Background(), filter)
対策方法
なので対策としては前述の気をつけるべきケースを避けることです。
- リクエスト受付時にユーザ入力値をvalidate(サニタイズ)しておく
- bson.M、bson.Dを使用して、ユーザの入力は値に留める
- bson.Mまるごとバインドしない
- ユーザの入力をキーとして使用しない
- $whereのような自由度の高いクエリを許可しない
まとめ
mongo-go-driverは基本的な使い方であれば、NoSQLインジェクションに対して強いと言えるでしょう。
しかしながら適切な使い方をしなければ当然脆弱性が生まれるので、注意が必要です。