Aplikasi menerima input dari pengguna, lalu menjadikannya bagian dari query. Di titik pertemuan inilah injection hidup: penyerang menyusupkan operator atau potongan logikanya sendiri agar query yang dibentuk berbeda dari maksud aplikasi. Keluarga serangan ini bukan monopoli database SQL — MongoDB punya dua pintu masuknya sendiri: operator injection melalui objek filter yang dibentuk begitu saja dari input, dan JavaScript injection melalui $where. Kabar baiknya: keduanya tertutup rapat oleh satu kebiasaan — validasi input dan filter yang selalu dibangun secara eksplisit oleh kode, bukan dirangkai dari string mentah.

Peta materi Chapter ini:

  • Operator injection: bagaimana { user: req.query.user } bisa membocorkan seluruh collection.
  • Serangan klasik bypass login dengan $ne.
  • $where dan risiko JavaScript di server.
  • Pola pertahanan: whitelist, validasi tipe, dan pembentukan filter eksplisit.

Operator Injection: Input yang Berubah Menjadi Operator #

Bayangkan endpoint pencarian pelanggan di Express yang mengambil filter langsung dari query string:

js
// mdtest: skip
// Pola BERBAHAYA: req.query langsung menjadi filter MongoDB
app.get("/pelanggan", async (req, res) => {
  const hasil = await db.collection("pelanggan").find(req.query).toArray();
  res.json(hasil);
});

Query HTTP biasa seperti ?nama=Budi memang menjadi filter { nama: "Budi" } — tepat seperti yang diinginkan. Tetapi query string juga bisa mengirimkan struktur: ?nama[$ne]= menghasilkan objek { nama: { $ne: "" } }. Filter itu cocok dengan setiap document yang nama-nya bukan string kosong — dan tiba-tiba endpoint pencarian Anda mengembalikan seluruh isi collection. Operator lain berbahaya bila lepas kendali: $gt, $regex, $exists, hingga $where bila driver menerimanya.

Mekanisme di baliknya sederhana namun sering luput: query string parsing mengubah kurung siku menjadi objek bersarang, dan driver MongoDB menerima objek apa pun sebagai filter — termasuk objek yang memuat operator. Aplikasi tidak pernah bermaksud memberi pengguna hak menulis operator; kelalaian bentuk itulah yang dieksploitasi.

Bypass Login Klasik dengan $ne #

Bentuk paling terkenal dari serangan ini menargetkan form login. Autentikasi naif membentuk filter dari username dan password mentah:

js
// mdtest: skip
// Pola BERBAHAYA: filter dari input mentah
const user = await db.collection("users").findOne({
  username: req.body.username,
  password: req.body.password,
});
if (user) grantAccess();

Penyerang mengirim username: { "$ne": "" } dan password: { "$ne": "" } — misalnya lewat body JSON. Filter yang terbentuk kira-kira berbunyi: “pengguna bernama apa pun yang bukan string kosong, dengan password apa pun yang bukan string kosong”. Karena collection memuat minimal satu akun, findOne mengembalikan akun pertama itu, dan grantAccess() terpanggil — tanpa password yang benar pernah diketahui.

Akar masalahnya bukan MongoDB, melainkan tipe yang lolos tanpa pemeriksaan: $ne adalah objek, padahal aplikasi hanya boleh menerima string. Inilah mengapa pertahanan pertamanya adalah validasi tipe.

$where: JavaScript di Dalam Server #

MongoDB menyediakan $where untuk kondisi berbentuk ekspresi JavaScript yang dieksekusi di server. Siapkan dua pelanggan contoh terlebih dahulu:

js
use latihan_injeksi
db.pelanggan.drop()
db.pelanggan.insertMany([
  { nama: "Budi Santoso", kota: "Bandung" },
  { nama: "Siti Nurhaliza Putri Ramadhani", kota: "Medan" }
])

Output yang Dihasilkan:

text
switched to db latihan_injeksi
true
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ad3c63546c34c7be7bf0001'),
    '1': ObjectId('6ad3c63546c34c7be7bf0002')
  }
}

Sekarang cari pelanggan yang namanya lebih panjang dari 25 karakter:

js
db.pelanggan.find({
  $where: "this.nama.length > 25"
})

Output yang Dihasilkan:

text
[
  {
    _id: ObjectId('6ad3c63546c34c7be7bf0002'),
    nama: 'Siti Nurhaliza Putri Ramadhani',
    kota: 'Medan'
  }
]

Query di atas sah dan hasilnya wajar: hanya nama Siti yang melebihi 25 karakter. Bahayanya muncul saat bagian string $where dirangkai dari input pengguna — penyerang cukup menutup kutip dan menyusupkan kode JavaScript penuh, dan server mengeksekusinya. Karena itu $where hampir selalu bisa dan sebaiknya digantikan operator biasa:

js
db.pelanggan.find({
  $expr: { $gt: [ { $strLenCP: "$nama" }, 25 ] }
})

Output yang Dihasilkan:

text
[
  {
    _id: ObjectId('6ad3c63546c34c7be7bf0002'),
    nama: 'Siti Nurhaliza Putri Ramadhani',
    kota: 'Medan'
  }
]

$expr mengevaluasi ekspresi aggregation yang aman secara struktur — tidak ada string yang dikompilasi menjadi kode. Aturan praktisnya: gunakan $where hanya untuk kondisi yang benar-benar mustahil dinyatakan dengan operator lain, dan jangan pernah letakkan input pengguna di dalam string $where.

Pola Pertahanan #

Pertahanan yang kuat terdiri dari tiga lapis yang saling menguatkan:

  1. Validasi tipe dan bentuk sejak garis depan. Username yang diharapkan string harus ditolak bila datang sebagai objek. Kerangka web modern (Zod, Joi, Pydantic) memaksa input sesuai skema sebelum menyentuh lapisan data.
  2. Bangun filter secara eksplisit, field per field. Ambil nilai yang sudah tervalidasi, lalu susun objek filter baru di dalam kode — jangan pernah meneruskan objek mentah dari request sebagai filter:
js
// mdtest: skip
// Pola AMAN: filter dibangun eksplisit dari nilai yang sudah tervalidasi
const { nama, kota } = await validateSchema(req.query);
const filter = {};
if (nama) filter.nama = nama;         // string murni, bukan objek
if (kota) filter.kota = kota;
const hasil = await db.collection("pelanggan").find(filter).toArray();
  1. Whitelist operator bila filter memang perlu fleksibel. Untuk fitur pencarian lanjutan yang memang mengizinkan operator terbatas (misalnya rentang harga), terjemahkan parameter khusus menjadi operator secara eksplisit: hargaMin: 50000 → { harga: { $gte: 50000 } }. Operator tetap ditulis oleh kode Anda, bukan dipilih oleh pengguna.

Rangkuman #

  • Operator injection terjadi ketika input pengguna membentuk objek filter berisi operator ($ne, $gt, $regex) yang tidak dikehendaki aplikasi.
  • Bypass login klasik mengirim username: {"$ne": ""} — filter cocok dengan akun pertama tanpa password yang benar.
  • $where mengeksekusi string JavaScript di server; ganti dengan operator biasa atau $expr, dan jangan pernah menyusupkan input pengguna ke string $where.
  • Tiga lapis pertahanan: validasi tipe sejak awal, filter dibangun eksplisit field per field, dan whitelist operator untuk fitur pencarian lanjutan.