Menguak Ancaman Prototype Pollution: Kerentanan JavaScript yang Sering Terlupakan di Aplikasi Web Anda
1. Pendahuluan
Di dunia pengembangan web yang serba cepat, fokus kita seringkali tertuju pada fitur-fitur baru, performa, dan pengalaman pengguna. Namun, ada satu area krusial yang tidak boleh terlewatkan: keamanan. Kita sudah sering mendengar tentang XSS, CSRF, atau SQL Injection, tapi bagaimana dengan kerentanan yang lebih “silent” dan spesifik JavaScript? Salah satunya adalah Prototype Pollution.
📌 Apa itu Prototype Pollution?
Bayangkan Anda memiliki resep dasar untuk semua kue Anda (Object.prototype). Resep ini berisi instruksi umum seperti “cara memanggang” atau “cara menghias”. Prototype Pollution adalah kondisi di mana penyerang bisa menyuntikkan atau memodifikasi instruksi di resep dasar ini, sehingga semua kue baru yang Anda buat (objek JavaScript baru) akan secara otomatis mengikuti instruksi yang telah dimanipulasi oleh penyerang.
Kerentanan ini muncul ketika aplikasi JavaScript (baik di frontend maupun backend Node.js) secara tidak sengaja mengizinkan pengguna untuk memodifikasi prototipe dasar objek, khususnya Object.prototype. Karena hampir semua objek di JavaScript mewarisi dari Object.prototype, perubahan pada prototipe ini bisa memiliki dampak global dan berbahaya.
Mengapa penting untuk developer web? Kerentanan ini bisa berujung pada berbagai serangan serius, mulai dari Denial of Service (DoS), bypass otorisasi, hingga Remote Code Execution (RCE) di sisi server Node.js. Sayangnya, banyak developer yang belum sepenuhnya memahami mekanisme atau potensi dampaknya, sehingga sering terlewatkan dalam proses pengembangan dan peninjauan kode.
Mari kita selami lebih dalam!
2. Memahami JavaScript Prototype Chain: Fondasi Kerentanan
Sebelum kita bisa memahami Prototype Pollution, kita perlu menyegarkan kembali pemahaman tentang bagaimana JavaScript bekerja dengan objek dan pewarisan.
Di JavaScript, setiap objek memiliki sebuah prototipe (prototype), dan prototipe ini juga bisa memiliki prototipe, membentuk apa yang disebut prototype chain. Ketika Anda mencoba mengakses properti dari sebuah objek, JavaScript akan mencarinya di objek itu sendiri. Jika tidak ditemukan, ia akan naik ke prototipe objek tersebut, dan seterusnya, hingga mencapai Object.prototype (prototipe paling dasar) atau null.
const obj = {};
// obj.__proto__ === Object.prototype
// Object.prototype.__proto__ === null
const myArray = [];
// myArray.__proto__ === Array.prototype
// Array.prototype.__proto__ === Object.prototype
// Object.prototype.__proto__ === null
Properti __proto__ (atau [[Prototype]] secara internal) adalah referensi ke objek prototipe. Object.prototype adalah objek yang menjadi akar dari sebagian besar rantai prototipe objek di JavaScript.
💡 Contoh Sederhana Pewarisan:
const animal = {
eats: true,
walk() {
console.log("Animal walks.");
}
};
const rabbit = {
jumps: true,
__proto__: animal // Mengatur 'animal' sebagai prototipe 'rabbit'
};
rabbit.walk(); // Output: Animal walks. (diwarisi dari animal)
console.log(rabbit.eats); // Output: true (diwarisi dari animal)
console.log(rabbit.jumps); // Output: true (milik rabbit sendiri)
Dalam contoh di atas, rabbit tidak memiliki metode walk() secara langsung, tetapi ia dapat mengaksesnya karena animal adalah bagian dari prototype chain-nya.
Kerentanan Prototype Pollution memanfaatkan fakta bahwa jika penyerang bisa memodifikasi Object.prototype, maka properti atau metode baru yang disuntikkan akan tersedia untuk semua objek baru yang dibuat menggunakan literal objek {} atau new Object(), dan juga untuk objek yang secara implisit mewarisi dari Object.prototype.
3. Bagaimana Prototype Pollution Bekerja? Anatomi Serangan
Serangan Prototype Pollution terjadi ketika ada fungsi dalam aplikasi Anda yang secara rekursif melakukan deep merge (menggabungkan properti dari beberapa objek secara mendalam) atau clone objek, dan fungsi tersebut tidak secara hati-hati menangani properti khusus seperti __proto__ atau constructor.prototype.
❌ Kondisi Umum Kerentanan: Sebuah aplikasi rentan jika:
- Menerima input yang dikontrol pengguna (misalnya dari body JSON, query string, atau form data).
- Menggunakan fungsi utilitas (baik kustom maupun dari library pihak ketiga yang belum di-patch) yang melakukan deep merge atau extend objek secara rekursif.
- Fungsi merge/extend tersebut tidak memiliki validasi atau sanitasi yang memadai untuk properti
__proto__atauconstructor.prototype.
⚠️ Payload Khas Serangan: Penyerang biasanya akan menyertakan salah satu dari dua bentuk payload dalam input mereka:
{"__proto__": {"maliciousProperty": "value"}}{"constructor": {"prototype": {"maliciousProperty": "value"}}}
Payload ini, ketika diproses oleh fungsi merge yang rentan, akan “menyuntikkan” maliciousProperty langsung ke Object.prototype.
Mari kita lihat contoh kode JavaScript yang rentan:
// ❌ CONTOH KODE RENTAN: Implementasi deep merge yang tidak aman
function unsafeDeepMerge(target, source) {
for (const key in source) {
// ⚠️ Masalah utama: Tidak ada validasi untuk '__proto__' atau 'constructor'
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) {
target[key] = {};
}
unsafeDeepMerge(target[key], source[key]); // Rekursif
} else {
target[key] = source[key];
}
}
}
return target;
}
const defaultConfig = {
env: 'development',
db: {
host: 'localhost',
port: 5432
}
};
// Input dari pengguna yang dikontrol oleh penyerang
// Penyerang mencoba menyuntikkan properti 'isAdmin' ke Object.prototype
const maliciousInput = JSON.parse('{"db": {"host": "malicious.com"}, "__proto__": {"isAdmin": true}}');
// Mencoba menggabungkan input pengguna ke konfigurasi
// Anggap saja 'defaultConfig' adalah objek yang ingin kita gabungkan
// Untuk demonstrasi, kita akan mulai dengan objek kosong agar lebih jelas dampaknya
const finalConfig = {};
unsafeDeepMerge(finalConfig, defaultConfig); // Gabungkan default dulu
unsafeDeepMerge(finalConfig, maliciousInput); // Kemudian gabungkan input berbahaya
console.log('Final Config:', finalConfig);
// Output: { env: 'development', db: { host: 'malicious.com', port: 5432 } }
// Objek 'finalConfig' terlihat normal. Tapi apa yang terjadi di balik layar?
// Sekarang, buat objek baru. Apakah properti 'isAdmin' muncul?
const newUser = { name: 'Alice' };
console.log('newUser.isAdmin:', newUser.isAdmin); // Output: true!
// Bahkan objek kosong pun kini memiliki properti ini
console.log('{}.isAdmin:', {}.isAdmin); // Output: true!
// 💡 Catatan: Object.create(null) tidak akan terpengaruh karena tidak mewarisi dari Object.prototype.
console.log('Object.create(null).isAdmin:', Object.create(null).isAdmin); // Output: undefined
Dalam contoh di atas, meskipun finalConfig terlihat normal, Object.prototype telah dimodifikasi. Properti isAdmin: true sekarang ada di prototipe dasar, sehingga setiap objek baru yang dibuat dengan literal {} atau new Object() akan secara otomatis mewarisi isAdmin: true.
🎯 Dampak yang Mungkin Terjadi:
- Bypass Otorisasi/Autentikasi: Seperti contoh di atas, jika aplikasi Anda memeriksa
user.isAdminuntuk otorisasi, penyerang bisa mendapatkan akses admin tanpa izin. - Denial of Service (DoS): Penyerang bisa memodifikasi properti atau metode krusial di
Object.prototype(misalnya,toString,hasOwnProperty) menjadi nilai yang tidak valid (misalnya, string atau objek alih-alih fungsi). Ini akan menyebabkan aplikasi crash di mana saja ketika properti/metode tersebut dipanggil. - Remote Code Execution (RCE) di Node.js: Ini adalah skenario paling berbahaya. Jika aplikasi backend Node.js Anda menggunakan library yang melakukan evaluasi kode berdasarkan properti objek (misalnya, template engine), penyerang bisa menyuntikkan kode berbahaya ke
Object.prototypeyang kemudian dieksekusi oleh library tersebut. - Modifikasi Konfigurasi Aplikasi: Menimpa variabel konfigurasi global yang sensitif.
4. Skenario Serangan di Aplikasi Web
Prototype Pollution bukan hanya ancaman teoritis; ia bisa dieksploitasi dalam berbagai konteks aplikasi web.
4.1. Frontend (Browser)
Di sisi klien, Prototype Pollution seringkali dikombinasikan dengan kerentanan lain seperti Cross-Site Scripting (XSS) atau di lingkungan di mana ada input pengguna yang tidak divalidasi dengan baik.
- Bypass Logic UI: Penyerang dapat memodifikasi properti yang digunakan oleh JavaScript frontend untuk mengontrol tampilan atau perilaku UI. Misalnya, menyuntikkan
isHidden: truekeObject.prototypebisa membuat beberapa elemen UI penting tidak terlihat oleh pengguna, atau sebaliknya. - Manipulasi Data: Mengubah cara data diproses atau ditampilkan, berpotensi memicu perilaku yang tidak diinginkan.
- Penyalahgunaan Library Frontend: Jika aplikasi menggunakan library JavaScript yang rentan terhadap Prototype Pollution, penyerang bisa memanipulasi perilaku library tersebut untuk tujuan jahat.
4.2. Backend (Node.js)
Sisi backend dengan Node.js adalah tempat Prototype Pollution bisa paling merusak, terutama karena potensi RCE.
- Remote Code Execution (RCE): Banyak library Node.js, terutama yang menangani parsing data atau template rendering, secara implisit mengakses properti objek. Jika penyerang bisa menyuntikkan kode berbahaya ke
Object.prototype(misalnya melalui properti yang dievaluasi olehchild_process.execatau template engine), mereka bisa mengeksekusi perintah di server. - Denial of Service (DoS): Seperti yang disebutkan, menimpa fungsi penting seperti
toString()atauhasOwnProperty()diObject.prototypedengan nilai non-fungsi dapat menyebabkan crash aplikasi secara luas. - Bypass Otorisasi Server-Side: Jika logic otorisasi backend memeriksa properti di objek yang mungkin terpengaruh (misalnya,
req.user.isAdmin), penyerang bisa mendapatkan akses yang tidak sah. - Manipulasi Konfigurasi Server: Mengubah pengaturan server yang sensitif, seperti kredensial database atau alamat API.
5. Strategi Pencegahan dan Mitigasi ✅
Melindungi aplikasi Anda dari Prototype Pollution membutuhkan pendekatan berlapis.
5.1. ✅ Validasi dan Sanitasi Input Ketat
Ini adalah pertahanan pertama dan terpenting.
- Jangan Percaya Input Pengguna: Selalu asumsikan input dari pengguna adalah berbahaya.
- Whitelist Properti: Alih-alih blacklist, gunakan whitelist untuk properti yang diizinkan dalam objek yang Anda proses dari input pengguna. Ini berarti Anda hanya menerima properti yang secara eksplisit Anda harapkan.
- Gunakan Schema Validation: Tools seperti Zod atau Joi sangat efektif untuk memastikan struktur dan tipe data input sesuai harapan Anda, mencegah properti tak terduga seperti
__proto__atauconstructormasuk ke dalam logic merge.
5.2. ✅ Hindari Deep Merge Rekursif yang Tidak Aman
- Shallow Merge: Untuk menggabungkan objek di level atas, gunakan
Object.assign()atau spread operator ({ ...obj1, ...obj2 }). Ini tidak melakukan deep merge dan aman dari Prototype Pollution. - Library Deep Merge yang Terpercaya: Jika Anda memang membutuhkan deep merge, gunakan library yang sudah terbukti aman dan selalu perbarui ke versi terbaru. Contoh:
lodash.merge(pastikan versi terbaru yang sudah di-patch).immeruntuk manajemen state yang aman dan imutabel.
- Implementasi Deep Merge Kustom yang Aman: Jika Anda harus menulis fungsi deep merge sendiri:
// ✅ CONTOH KODE AMAN: Implementasi deep merge yang memeriksa '__proto__' dan 'constructor' function safeDeepMerge(target, source) { if (target === null || typeof target !== 'object' || Array.isArray(target)) { // Target harus objek, jika tidak, kembalikan source atau buat objek baru return source; } for (const key in source) { // ✅ Pemeriksaan kunci yang krusial if (key === '__proto__' || key === 'constructor') { continue; // Abaikan properti berbahaya } if (Object.prototype.hasOwnProperty.call(source, key)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key] || typeof target[key] !== 'object' || Array.isArray(target[key])) { target[key] = {}; // Pastikan target[key] juga objek untuk rekursi } safeDeepMerge(target[key], source[key]); } else { target[key] = source[key]; } } } return target; } const config = {}; safeDeepMerge(config, defaultConfig); safeDeepMerge(config, maliciousInput); // Input berbahaya tidak akan mempolusi console.log('Config setelah safe merge:', config); console.log('{}.isAdmin setelah safe merge:', {}.isAdmin); // Output: undefined (aman!)
5.3. ✅ Gunakan Object.freeze(), Object.seal(), atau Object.preventExtensions()
Untuk objek-objek penting atau konfigurasi global, Anda bisa membuatnya tidak dapat diubah (immutable).
Object.freeze(obj): Membuat objek tidak bisa dimodifikasi (properti tidak bisa ditambah, dihapus, atau diubah nilainya).Object.seal(obj): Properti tidak bisa ditambah atau dihapus, tapi nilainya bisa diubah.Object.preventExtensions(obj): Properti tidak bisa ditambah, tapi bisa dihapus atau diubah nilainya.
Membekukan Object.prototype itu sendiri di awal aplikasi bisa menjadi langkah ekstrem tapi efektif untuk mencegah polusi. Namun, ini bisa memiliki efek samping yang tidak terduga pada library pihak ketiga yang mungkin mencoba memodifikasinya.
5.4. ✅ Perbarui Dependensi Secara Teratur
Kerentanan Prototype Pollution sering ditemukan dan di-patch di library populer seperti Lodash, jQuery, atau Express. Pastikan Anda selalu menggunakan versi terbaru dari semua dependensi proyek Anda. Tools seperti npm audit atau yarn audit dapat membantu mengidentifikasi dependensi yang rentan.
5.5. ✅ Gunakan Map atau WeakMap untuk Penyimpanan Data
Jika Anda menyimpan data yang mungkin berasal dari input pengguna dan tidak memerlukan pewarisan prototipe, pertimbangkan untuk menggunakan Map alih-alih objek biasa. Map tidak menggunakan prototype chain dan kebal terhadap serangan Prototype Pollution.
6. Mendeteksi Prototype Pollution
Mendeteksi kerentanan Prototype Pollution bisa jadi rumit karena sifatnya yang tersembunyi.
- Code Review Manual: Lakukan tinjauan kode secara cermat, terutama pada bagian yang:
- Menerima input JSON atau form data dari pengguna.
- Mel