یک بار در تست امنیتی یکی از سایت‌های مشتری، به فرمی برخوردم که همه ورودی‌ها را از نظر SQL بررسی می‌کرد ولی هیچ توجهی به خروجی نداشت. همان لحظه یادداشت کردم: مشکل این سایت پاک‌سازی ورودی نیست، خروجی است. چند هفته بعد همان حفره به XSS (Cross-Site Scripting) منتهی شد. از آن روز، در هر بازبینی امنیتی، بعد از کوئری‌ها سراغ خروجی‌ها می‌روم. این نوشته درباره همان آسیب‌پذیری است که گاهی از SQL Injection بی‌سروصداتر ولی در عمل خطرناک‌تر است.

XSS چیست و چطور کار می‌کند؟

XSS یا Cross-Site Scripting، یک آسیب‌پذیری است که در آن مهاجم موفق می‌شود کد جاوااسکریپت خودش را در صفحه‌ای که کاربر قربانی می‌بیند اجرا کند. طبق توضیح ویکی‌پدیای فارسی درباره اسکریپت‌نویسی مقطعی، ریشه این حمله در اعتماد مرورگر به کدی است که از سرور می‌آید. اگر بخشی از آن کد از ورودی کاربر ساخته شده باشد، مهاجم می‌تواند دستورات دلخواه خودش را داخل همان کد بگنجاند.

برای فهم دقیق‌تر مکانیزم پایه، مقاله XSS چیست و چگونه دفع می‌شود را ببینید. اما خلاصه ماجرا: مسئله در جایی به وجود می‌آید که داده به عنوان کد تفسیر می‌شود. هر جایی که کد شما ورودی کاربر را در HTML، در attribute یا در جاوااسکریپت قرار می‌دهد، اگر خروجی را از نظر زمینه‌ای که در آن قرار می‌گیرد بررسی نکنید، راه برای XSS باز است.

در XSS، مشکل نرم‌افزار شما نیست؛ مشکل اعتماد مرورگر به چیزی است که شما تولید کرده‌اید.

انواع XSS: Stored، Reflected و DOM-based

سه نوع اصلی XSS وجود دارد که هر کدام در سناریوی متفاوتی رخ می‌دهد:

  • Stored XSS (ذخیره‌شده): مخرب‌ترین نوع، چون کد مخرب روی سرور ذخیره می‌شود و هر بازدیدکننده صفحه‌ای که آن داده را می‌بیند، قربانی می‌شود. مثال کلاسیک: دیدگاهی که شامل تگ script است و به درستی پاک‌سازی نشده.
  • Reflected XSS (بازتابی): کد مخرب از طریق URL یا پارامتر ورودی می‌آید و همان لحظه در پاسخ صفحه بازتاب می‌شود. قربانی باید روی یک لینک مخرب کلیک کند.
  • DOM-based XSS: کد مخرب در سمت مرورگر و توسط جاوااسکریپت سایت اجرا می‌شود، بدون آنکه درخواست به سرور برود. مثال: کدی که location.hash را مستقیم در innerHTML می‌ریزد.

نوع DOM-based در پروژه‌های مدرن، به خصوص در SPAها، بیشترین شیوع را دارد. چون در این نوع، حتی اگر سرور خروجی را کامل پاک‌سازی کند، جاوااسکریپت سمت کلاینت می‌تواند دوباره آن را به HTML برگرداند.

چرا XSS خطرناک‌تر از تصور اولیه است؟

خیلی‌ها XSS را با یک popup ساده برابر می‌گیرند. در واقعیت، XSS کلید ورود به دنیایی از حملات است: دزدیدن session cookie و برداشتن اکانت کاربر، ریدایرکت مخفی، تزریق فرم جعلی برای گرفتن رمز عبور، اجرای درخواست‌های از طرف کاربر، و حتی با ترکیب با آسیب‌پذیری‌های دیگر، در دست گرفتن پنل مدیریت سایت. برای مقایسه با انواع دیگر حملات، مقاله CSRF چیست و چطور از آن جلوگیری کنیم را ببینید؛ XSS و CSRF در خیلی از سناریوها مکمل یکدیگر می‌شوند.

اصل طلایی: پاک‌سازی خروجی، نه فقط ورودی

یکی از رایج‌ترین باورهای غلط این است که اگر ورودی را پاک‌سازی کنیم، خروجی امن است. این باور در نیمی از سناریوهای واقعی نادرست است. دلیلش این است که داده معتبر در یک زمینه، می‌تواند در زمینه دیگر مخرب باشد. مثلاً اسم یک کاربر که شامل کاراکتر خاص است ممکن است برای نام کاربری معتبر ولی در یک attribute HTML خطرناک باشد. بنابراین اصل طلایی امنیت وب این است که پاک‌سازی و کدگذاری، دقیقاً در لحظه خروجی انجام شود و بر اساس زمینه‌ای که داده در آن قرار می‌گیرد.

این رویکرد را در پروژه‌ها به عنوان Separation of Concerns می‌شناسم: داده خام در دیتابیس ذخیره می‌شود، و لایه نمایشی هر بار که آن را به HTML، attribute یا جاوااسکریپت تبدیل می‌کند، آن را کدگذاری می‌کند. برای مطالعه بیشتر درباره لایه‌های کلی امنیت، نوشتن کد PHP امن را از ابتدا بخوانید.

کدگذاری بر اساس زمینه (Context)

خروجی داده در HTML، در attribute، در URL و در جاوااسکریپت هر کدام قاعده خاص خودش را دارند. کدگذاری HTML برای متن، کافی نیست اگر همان داده در داخل یک attribute بیاید. یک نمونه ساده:

<a href="/user?name=<?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?>">...</a>

در این کد، htmlspecialchars با فلگ ENT_QUOTES هم کوتیشن تک و هم دوتایی را کدگذاری می‌کند تا اگر داده درون attribute دوبل یا تک قرار گرفت، فرار نکند. اگر داده درون جاوااسکریپت قرار بگیرد، کدگذاری متفاوتی لازم است؛ معمولاً باید از json_encode با فلگ امن استفاده شود و داده درون یک متغیر جاوااسکریپت قرار بگیرد، نه مستقیم در متن HTML.

نقش CSP در دفاع لایه‌ای

CSP یا Content Security Policy، یک لایه دفاعی اضافه در سمت مرورگر است که مشخص می‌کند چه اسکریپت‌هایی مجاز به اجرا در صفحه هستند. یک CSP درست نوشته شده می‌تواند حتی اگر یک XSS هم رخ بدهد، اثر آن را خنثی کند. ولی CSP جایگزین کدگذاری صحیح نیست؛ یک لایه مکمل است. قانون سرانگشتی من: هیچ‌وقت روی یک لایه امنیتی تکیه نکنید، همیشه دو یا سه لایه داشته باشید.

جلوگیری از XSS در وردپرس

وردپرس توابع اختصاصی برای کدگذاری و پاک‌سازی خروجی دارد: esc_html()، esc_attr()، esc_url()، wp_kses_post() و چند تابع دیگر. هر خروجی باید با تابع مناسب زمینه‌اش کدگذاری شود. برای مطالعه دقیق این توابع، توابع وردپرس برای امنیت و پاک‌سازی داده‌ها را ببینید. در سمت ورودی، توابعی مثل sanitize_text_field، sanitize_email و wp_kses برای انواع داده وجود دارند که در مقاله پاک‌سازی داده‌ها در کدنویسی وردپرس مفصل توضیح داده‌ام.

یک نکته مهم که در پروژه‌های واقعی خیلی دیدم: خیلی از افزونه‌ها ورودی را درست پاک‌سازی می‌کنند ولی خروجی را بدون esc_html چاپ می‌کنند. این اشتباه، در عمل XSS ذخیره‌شده ایجاد می‌کند. راه‌حل ساده: هر بار echo نوشتید، از خودتان بپرسید این داده از کجا آمده و آیا با تابع مناسب کدگذاری شده.

XSS در جاوااسکریپت مدرن و SPA

در برنامه‌های تک‌صفحه‌ای (Single Page Application)، بخش بزرگی از رندر در مرورگر اتفاق می‌افتد. اگر با React، Vue یا فریم‌ورک‌های مشابه کار می‌کنید، بدانید که این فریم‌ورک‌ها در حالت پیش‌فرض از XSS محافظت می‌کنند: متغیرها درون JSX یا template با escape چاپ می‌شوند. اما لحظه‌ای که از dangerouslySetInnerHTML یا v-html استفاده می‌کنید، آن حفاظت از بین می‌رود. برای مطالعه بیشتر درباره لایه فرانت‌اند، فرانت‌اند چیست را ببینید.

هر API که HTML می‌سازد، در واقع یک قرارداد اعتماد با مرورگر است؛ اگر این قرارداد را با داده کاربر بشکنید، XSS نتیجه‌اش است.

اشتباهات رایجی که در را باز می‌گذارد

  • اعتماد به ورودی فیلترشده: حتی اگر ورودی را پاک‌سازی کنید، ممکن است در زمینه خروجی جدید خطرناک شود.
  • استفاده از blacklist به‌جای whitelist: اگر فقط تگ script را مسدود کنید، مهاجم از onerror، onclick و دهها راه دیگر استفاده می‌کند.
  • فراموش کردن attributeها: خیلی از XSSها از طریق attribute و رویدادهایی مثل onmouseover اجرا می‌شوند نه از طریق تگ script.
  • بی‌توجهی به JSON درجاوااسکریپت: اگر داده کاربر را درون یک بلوک script قرار دهید بدون json_encode، حمله ممکن می‌شود.
  • عدم پیاده‌سازی CSP: CSP یک لایه دفاعی مهم است که خیلی از سایت‌ها آن را نادیده می‌گیرند.

پرسش‌های پرتکرار درباره XSS و دفاع در برابر آن

آیا XSS فقط به جاوااسکریپت محدود است؟

در عمل بله؛ مکانیزم اجرای کد در مرورگر با جاوااسکریپت است. اما مهاجم می‌تواند از تگ‌های دیگر مثل SVG و MathML هم برای اجرای جاوااسکریپت استفاده کند.

آیا کدگذاری HTML کافی است؟

نه به تنهایی. برای هر زمینه‌ای (HTML، attribute، URL، CSS، جاوااسکریپت) کدگذاری مناسب لازم است. تکیه بر یک تابع همیشه کافی نیست.

آیا افزونه‌های امنیتی جایگزین کدنویسی صحیح هستند؟

خیر. افزونه‌هایی مثل افزونه‌های امنیتی وردپرس می‌توانند لایه دفاعی اضافه اضافه کنند ولی ریشه XSS در کد است و باید در کد رفع شود.

آیا XSS در اپلیکیشن‌های موبایل هم رخ می‌دهد؟

بله، در WebView و در اپلیکیشن‌هایی که HTML نمایش می‌دهند، همان قواعد لازم است. برای مطالعه بیشتر درباره لایه رابط کاربری، طراحی رابط کاربری چیست و امن‌سازی لاگین وردپرس را ببینید.

حرف آخر درباره امنیت خروجی

XSS یکی از آن آسیب‌پذیری‌هایی است که همیشه از سمت جایی می‌آید که کسی به آن فکر نکرده. ممکن است ورودی پاک‌سازی شده باشد، ممکن است SQL Injection وجود نداشته باشد، ولی خروجی در یک attribute نادرست یا در یک بلوک جاوااسکریپت بدون کدگذاری مناسب، همان در را باز می‌گذارد. عادت واقعی که در این سال‌ها به من کمک کرده این است: هر جا داده‌ای از سمت کاربر به خروجی می‌رود، قبل از هر چیز از خودم بپرسم این داده در کدام زمینه قرار می‌گیرد و آیا کدگذاری مناسب را برای همان زمینه اعمال کرده‌ام. اگر تجربه‌ای از کشف یا رفع یک XSS در پروژه واقعی دارید، در دیدگاه‌ها بنویسید؛ جزئیات همان سناریوها برای خواننده‌های بعدی از هر مقاله آموزشی ارزشمندتر است.