یک باگ تزریق HTML ذخیره‌شده در تابلوی شغلی RamenHire به هر کسی اجازه می‌داد تا یک فرم عمومی را پر کند و ایمیل‌های اطلاع‌رسانی تیم را به یک پلتفرم فیشینگ تبدیل کند.

چگونه این باگ نفوذ کرد

RamenHire بر پایه Next.js و Supabase اجرا می‌شود و به استارتاپ‌ها اجازه می‌دهد بدون ورود به سیستم، آگهی‌های شغلی خود را منتشر کنند. هر ارسال فرم، باعث ارسال یک ایمیل به تیم استخدام داخلی می‌شود. بدنه ایمیل از طریق چسباندن مستقیم فیلدهای خام فرم — نام شرکت، شرح شغل، نام مخاطب — در یک رشته HTML ساخته می‌شود. قبل از اینکه این رشته به سرویس ایمیل برسد، هیچ فرآیند پاک‌سازی (sanitisation) یا خروج از کاراکترهای خاص (escaping) انجام نمی‌شود.

از آنجایی که قالب (template) با ورودی کاربر به عنوان HTML امن برخورد می‌کند، یک متقاضی مخرب می‌تواند یک لینک جعلی «برای تأیید اینجا کلیک کنید» تزریق کند. این محموله (payload) به عنوان بخشی از آگهی شغلی در سرور ذخیره می‌شود، بنابراین هر ایمیل اطلاع‌رسانی بعدی حاوی کد مخرب خواهد بود. یک مهاجم می‌تواند آن لینک را در کنار دکمه واقعی «مشاهده داشبورد» پنهان کند و یکی از اعضای تیم را فریب دهد تا اطلاعات کاربری خود را فاش کرده یا از یک سایت فیشینگ بازدید کند.

چرا این موضوع اهمیت داشت

این ایمیل‌ها برای تیم اصلی استخدام ارسال می‌شوند؛ افرادی که مدام برای پیشبرد کاندیداها در مراحل مختلف فرآیند استخدام، روی لینک‌ها کلیک می‌کنند.

شکاف فنی

خروج از کاراکترهای خاص (Escaping) در HTML یک مرحله ساده نیست. دو زمینه (context) نیاز به محافظت دارند:

  • گره‌های متنی (Text nodes) – محتوای بین تگ‌ها. تبدیل < به &lt; و > به &gt; جلوی تگ‌ها را می‌گیرد، اما مانع از آن نمی‌شود که یک مهاجم از مقدار یک ویژگی (attribute) خارج شود.
  • مقادیر ویژگی (Attribute values) – رشته‌های داخل تگ‌ها مانند href="…". تزریق یک علامت نقل‌قول (" یا ') به مهاجم اجازه می‌دهد تا ویژگی را زودتر از موعد ببندد و نشانه‌گذاری (markup) خود را وارد کند.

کد اصلی فقط متن ساده را مدیریت می‌کرد و راه را برای تزریق ویژگی (attribute injection) کاملاً باز گذاشته بود.

رفع مشکل

توسعه‌دهنده یک تابع کمکی کوچک به نام escapeHtml اضافه کرد و آن را قبل از جایگذاری (interpolation)، برای هر مقدار ارسالی توسط کاربر اعمال کرد؛ چه آن مقدار در یک گره متنی قرار بگیرد و چه در یک ویژگی.

مراحل تأیید

  • تست محلی – مجموعه‌ای از رشته‌های تزریقی به توابع قالب داده شد. هر تست تنها کاراکترهای خنثی‌شده (escaped) را نشان داد و هیچ تگ قابل اجرایی وجود نداشت.
  • تست در محیط عملیاتی – پس از اعمال وصله (patch)، یک محموله از طریق سایت زنده ارسال شد که از بررسی ربات Cloudflare Turnstile نیز عبور کرد. ایمیل حاصل به صندوق ورودی توسعه‌دهنده رسید؛ لینک مخرب و هرگونه تگ اسکریپت به صورت متن نامفهوم ظاهر شدند، نه به عنوان عناصر قابل کلیک.

نکته‌ای درباره Gmail: این سرویس ممکن است همچنان یک URL را آبی کرده و آن را قابل کلیک کند، اما این یک قابلیت در سمت کلاینت است و به معنای موفقیت‌آمیز بودن تزریق HTML نیست. این اصلاحیه مانع از آن می‌شود که تزریق، دکمه‌های واقعی ایجاد کند یا اسکریپت‌ها را اجرا نماید.

مواردی که توسعه‌دهندگان باید مراقب باشند

  • هرگز به داده‌های فرم‌های عمومی اعتماد نکنید – حتی فرم‌هایی که بی‌خطر به نظر می‌رسند نیز می‌توانند خروجی‌های ذخیره‌شده‌ای تولید کنند که در جاهای دیگر استفاده می‌شوند.
  • در نقطه استفاده، خنثی‌سازی (Escape) کنید – خنثی‌سازی وابسته به زمینه (متن در مقابل ویژگی) را دقیقاً قبل از رندر کردن اعمال کنید، نه در مراحل زودتر از خط لوله (pipeline).
  • هم سمت کلاینت و هم سمت سرور را تست کنید – تست‌های واحد (Unit tests) خطاهای آشکار را شناسایی می‌کنند؛ یک تست دستی سرتاسری (end-to-end) از طریق رابط کاربری زنده، تأیید می‌کند که دفاع در برابر ترافیک واقعی پابرجا است.
  • از رفتارهای عجیب کلاینت‌های ایمیل آگاه باشید – برخی کلاینت‌ها به طور خودکار URLها را لینک می‌کنند که باعث ایجاد حس امنیت کاذب می‌شود. تأیید کنید که HTML زیربنایی حاوی هیچ عنصر فعالی نباشد.

خلاصه کلام

یک غفلت ساده در مدیریت ورودی کاربر، ایمیل‌های اطلاع‌رسانی روتین را به یک ابزار فیشینگ تبدیل کرد. معرفی یک روال جامع برای خنثی‌سازی HTML و تأیید اصلاحیه در محیط‌های محلی و عملیاتی، سلامت صندوق ورودی را بازگرداند. این اتفاق بر درسی همیشگی برای هر اپلیکیشن وب که HTML را از داده‌های غیرقابل اعتماد تولید می‌کند، تأکید دارد: پاک‌سازی (sanitisation) صحیح غیرقابل مذاکره است و نادیده گرفتن آن می‌تواند راهی مستقیم به صندوق ورودی کاربران شما باز کند.