NoSQL injection terjadi kalau input user yang seharusnya sebuah nilai biasa (string) ternyata dikirim sebagai object operator, dan database seperti MongoDB memperlakukan object itu sebagai aturan pencarian, bukan sebagai data. Bayangin kasir yang kamu kasih catatan: "cari pelanggan dengan nomor 123". Kalau catatannya diganti jadi "cari pelanggan dengan nomor berapa pun yang bukan kosong", kasir yang nurut bakal mengembalikan semua pelanggan. MongoDB adalah kasir yang nurut itu, dan {"$ne": null} adalah catatan "berapa pun yang bukan kosong".
🧪 Tebakan cepat: query login mengirim
passwordapa adanya kefindOne, dan user mengirimpasswordberisi{"$ne": null}. Berapa banyak user yang cocok? Tebak dulu, jawabannya ada di bagian "Kenapa query NoSQL bisa dibajak".
NoSQL database gak pakai SQL, tapi tetap bisa diinjeksi. Bedanya: di SQL input jahat menyelinap sebagai teks query, di NoSQL input jahat menyelinap sebagai operator.
Kenapa query NoSQL bisa dibajak
Query login yang rentan mengambil username dan password dari body request, lalu langsung mengirimnya ke database:
// RENTAN: body dikirim apa adanya
app.post("/login", async (req, res) => {
const user = await db.collection("users").findOne({
username: req.body.username,
password: req.body.password,
});
if (user) res.json({ ok: true });
});
Kalau body-nya {"username": "admin", "password": "rahasia"}, query mencari user dengan password persis "rahasia". Aman.
Tapi body adalah JSON, dan JSON bisa membawa object, bukan cuma teks. Kalau user mengirim:
{ "username": "admin", "password": { "$ne": null } }
maka req.body.password adalah object { $ne: null }. Bagi MongoDB, $ne artinya not equal (tidak sama dengan). Jadi query berubah arti jadi "cari user admin yang password-nya bukan null", dan password admin yang asli jelas bukan null. User admin cocok tanpa tahu password-nya. Itu jawaban tebakannya: cocok, dan login ditembus.
Operator lain yang sering disalahgunakan dengan cara serupa: $gt dan $gte (lebih besar dari), $regex (pola teks), dan $where (menjalankan JavaScript di server). Pola masalahnya sama: input yang harusnya nilai, datang sebagai operator.
Perbaikan: validasi tipe input
Akar masalahnya: aplikasi menerima object padahal mengharapkan string. Jadi periksa tipenya sebelum query:
app.post("/login", async (req, res) => {
const { username, password } = req.body;
if (typeof username !== "string" || typeof password !== "string") {
return res.status(400).json({ error: "Input tidak valid" });
}
const user = await db.collection("users").findOne({ username, password });
});
typeof password !== "string"bernilaitruekalau password berupa object{ $ne: null }, jadi request ditolak dengan status 400 sebelum menyentuh database.- Dengan password string biasa, pemeriksaan lolos dan query jalan normal.
Satu baris typeof ini menutup celah utamanya: operator gak pernah sampai ke query.
Perbaikan: schema validation
Lapisan yang lebih rapi: tetapkan schema yang memaksa tipe tiap field. Mongoose (atau Zod/Joi) menolak data yang gak sesuai bentuk:
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true },
});
Dengan type: String, Mongoose memperlakukan { $ne: null } sebagai nilai yang gagal cast ke string, bukan sebagai operator. Schema juga jadi satu tempat tunggal buat semua aturan tipe, jadi gak perlu menulis typeof di tiap endpoint.
Sanitasi sebagai lapisan tambahan
Sebagai jaring tambahan, ada library yang membuang key berbahaya dari input, misalnya yang diawali $:
const mongoSanitize = require("express-mongo-sanitize");
app.use(mongoSanitize()); // membuang key yang diawali $ dan yang mengandung titik
Ini berguna, tapi bukan pengganti validasi tipe. Lapisan paling kuat tetap memastikan tiap input bentuknya sesuai yang diharapkan. Kalau $where gak dibutuhkan, matikan juga kemampuan menjalankan JavaScript di server untuk memperkecil permukaan serangan.
⚠️ Jebakan yang sering ketemu
- Percaya body JSON itu pasti string. Body bisa membawa object atau array. Field yang harusnya string, periksa dengan
typeof. - Cuma memblokir
$ne. Ada$gt,$regex,$where, dan lainnya. Daftar larangan selalu ketinggalan; validasi tipe menutup semuanya sekaligus. - Mengira ORM pasti aman. Query lewat Mongoose aman kalau schema-nya memaksa tipe. Query mentah (
db.collection(...).findOne) tetap butuh validasi sendiri. - Membiarkan
$whereaktif.$wheremenjalankan JavaScript di server. Kalau gak dipakai, matikan.
TL;DR: NoSQL injection terjadi kalau input yang harusnya string datang sebagai object operator: password berisi {"$ne": null} membuat MongoDB mencari user yang password-nya bukan null, jadi admin cocok tanpa password yang benar. Cegah dengan memeriksa tipe input: if (typeof password !== "string") return 400, jadi operator gak pernah sampai ke query. Schema validation (Mongoose type: String) memaksa tipe di satu tempat. Sanitasi dan mematikan $where adalah lapisan tambahan, bukan pengganti validasi tipe.