یک محقق امنیتی به نام Dhruv برای ما ایمیل زد و یک مسیر XSS ذخیرهشده گزارش داد: کسی یک فایل PDF با جاوااسکریپت داخلش را به یک تیکت پشتیبانی FanDesk پیوست میکند، و وقتی کارشناس پشتیبانی آن را باز میکند، اسکریپت «در بستر همان اپلیکیشن» اجرا میشود.
وقتی بررسی کردیم چه دیدیم
مسیر دانلود در FanDesk از دسامبر ۲۰۲۵ همیشه Content-Disposition: attachment و application/octet-stream را اجباری کرده — این را با git blame چک کردیم. نمایشگر پیوست در فرانتاند هیچجا iframe، embed یا object ندارد؛ هر پیوست غیرتصویری از مسیر دانلود Blob رد میشود، نه رندر درونخطی. آپلودها هم از قبل با python-magic اعتبارسنجی میشوند، یعنی بایتهای واقعی فایل خوانده میشود نه نوعی که مرورگر ادعا میکند، و image/svg+xml دقیقاً به همین دلیل از فهرست مجاز کنار گذاشته شده — یک فایل SVG میتواند تگ <script> داشته باشد.
پس مسیری که Dhruv توصیف کرده بود احتمالاً همانطور که نوشته شده تکرار نمیشود. اما این دلیلی نیست که کد را همانطور رها کنیم.
با این حال چه چیزی را تغییر دادیم
اعتبارسنجی زمان آپلود فقط میگوید فایل وقتی آپلود شد چه بود. هیچچیز جلوی این را نمیگیرد که بایتهای ذخیرهشده بعداً از مسیر دیگری طور دیگری سرو شوند. این شکاف را با شناسایی دوبارهٔ نوع واقعی فایل در زمان دانلود هم بستیم، از همان بایتهای روی دیسک، نه نوعی که در دیتابیس ذخیره شده:
detected_type = magic.from_file(str(file_path), mime=True)
is_image = FileStorageService.renders_inline_safely(detected_type)فقط یک نوع تصویر مجاز بهصورت inline سرو میشود؛ هر PDF و هر چیزی که صراحتاً نمیشناسیم، مجبور به دانلود میشود، حالا با هدر X-Content-Type-Options: nosniff و Content-Security-Policy: default-src 'none'; sandbox روی پاسخ، تا حتی اگر مسیر کد دیگری در آینده فایل را مستقیم باز کند، مرورگر نتواند تصمیم دیگری بگیرد. چهار تست جدید برای همین منطق اضافه کردیم و همان روز روی پروداکشن رفت.
درس واقعی
گزارش باگی که دقیقاً همانطور که نوشته شده تکرار نمیشود، باز هم ارزش خواندن دقیق دارد. ایمیل Dhruv به یک شکاف واقعی اشاره کرد — نه یک شکاف قابلسوءاستفاده امروز، اما شکافی بین چیزی که در زمان آپلود اعتبارسنجی میکنیم و چیزی که در زمان دانلود بهش اعتماد میکنیم. بستن این شکاف یک بعدازظهر وقت گرفت. فهمیدن اینکه بهش نیاز داریم، توجه دقیق یک نفر دیگر را گرفت، و یک پاسخ تشکر از او را ارزشش داشت.