Carpe Diem

備忘録

mongo-go-driver はNoSQLインジェクションに対して強いのか

背景

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.Mbson.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インジェクションに対して強いと言えるでしょう。
しかしながら適切な使い方をしなければ当然脆弱性が生まれるので、注意が必要です。

参考