Prototype Pollution — Security

Refresh: Prototype Chain Di JavaScript, setiap object mewarisi property dari Object.prototype. Kalau kita membaca property yang tidak ada di object, JS…

Prototype pollution adalah bug khas JavaScript: sebuah fungsi merge menyalin key dari data user tanpa disaring, dan key bernama __proto__ membuatnya menulis ke cetak biru bersama semua object, bukan ke object tujuannya. Bayangin satu kantor yang semua karyawannya pakai satu buku peraturan bersama. Kalau seseorang menyelipkan satu baris "semua orang boleh masuk ruang server" ke buku itu, aturan itu berlaku buat semua karyawan sekaligus, termasuk yang baru masuk besok.

🧪 Tebakan cepat: sebuah deep merge yang gak menyaring key menerima {"__proto__": {"isAdmin": true}}. Setelah itu, ({}).isAdmin, object kosong yang baru dibuat, nilainya apa? Tebak dulu, jawabannya ada di bagian "Merge yang gak menyaring key".

Prototype: buku peraturan bersama

Setiap object biasa di JavaScript punya prototype, object lain tempat dia "meminjam" property yang gak dia punya sendiri. Untuk object biasa, prototype-nya adalah Object.prototype, dan semua object biasa memakai prototype yang sama itu.

const user = {};
console.log(user.toString);                          // function: dipinjam dari Object.prototype
console.log(Object.getPrototypeOf(user) === Object.prototype); // true
console.log(user.isAdmin);                           // undefined: gak ada di mana pun

Merge yang gak menyaring key

Fungsi deep merge menyalin isi satu object ke object lain, termasuk object di dalamnya. Versi naif ini menyalin semua key tanpa bertanya:

// RENTAN: semua key disalin, termasuk "__proto__"
function deepMerge(target, source) {
  for (const key in source) {
    if (typeof source[key] === "object") {
      target[key] = target[key] || {};
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

const settings = {};
deepMerge(settings, JSON.parse('{"__proto__": {"isAdmin": true}}'));
console.log(({}).isAdmin); // true

Ikuti langkahnya dengan input dari user tadi:

Langkah Yang terjadi
key = "__proto__" target[key] membaca prototype settings, yaitu Object.prototype
deepMerge(Object.prototype, {isAdmin: true}) merge masuk ke buku peraturan bersama
key = "isAdmin" Object.prototype.isAdmin = true

Hasilnya ({}).isAdmin bernilai true. Itu jawaban tebakannya. Object kosong yang baru dibuat ikut "punya" isAdmin: true, jadi kode seperti if (user.isAdmin) bisa meloloskan siapa saja. Key constructor dan prototype juga bisa membuka jalan yang sama (obj.constructor.prototype adalah Object.prototype), jadi ketiganya disaring.

Perbaikan 1: lewati key berbahaya

Periksa tiap key sebelum disalin, dan lewati yang ada di daftar terlarang dengan continue:

function safeMerge(target, source) {
  const blocked = ["__proto__", "constructor", "prototype"];

  for (const key of Object.keys(source)) {
    if (blocked.includes(key)) {
      continue; // dilewati, gak pernah disalin
    }
    target[key] = source[key];
  }

  return target;
}

console.log(safeMerge({ a: 1 }, { b: 2 }));                      // { a: 1, b: 2 }
safeMerge({}, JSON.parse('{"__proto__": {"polluted": true}}'));
console.log(({}).polluted);                                       // undefined

Perbaikan 2: wadah tanpa prototype

Cara lain: simpan data dari user di wadah yang memang gak punya prototype, jadi gak ada buku bersama yang bisa dicemari.

const store = Object.create(null); // object tanpa prototype
store.theme = "dark";
console.log(store.toString); // undefined: gak mewarisi apa pun

const prefs = new Map();     // Map: key apa pun cuma jadi key biasa
prefs.set("__proto__", { isAdmin: true });
console.log(({}).isAdmin);   // undefined

⚠️ Jebakan yang sering ketemu

TL;DR: Semua object biasa meminjam property dari satu prototype bersama, Object.prototype. Deep merge yang menyalin semua key membiarkan {"__proto__": {"isAdmin": true}} menulis ke sana, jadi ({}).isAdmin bernilai true untuk semua object. Cegah dengan memeriksa tiap key sebelum disalin: for (const key of Object.keys(source)), lalu if (blocked.includes(key)) continue; untuk __proto__, constructor, prototype. Untuk data dari user, pakai wadah tanpa prototype: Object.create(null) atau Map.

Yang akan kamu pelajari