NoSQL Injection — Security

Apa itu NoSQL Injection? Meskipun NoSQL database seperti MongoDB tidak menggunakan SQL, mereka tetap rentan terhadap injection attacks. NoSQL Injection…

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 password apa adanya ke findOne, dan user mengirim password berisi {"$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 });
});

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

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.

Yang akan kamu pelajari