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

طبق گزارش OWASP Top 10 در نسخه ۲۰۲۱، XSS همچنان به عنوان بخشی از دسته A03 (Injection) در فهرست ده آسیب‌پذیری بحرانی وب قرار دارد. طبق گزارش Imperva Bad Bot Report، حدود ۴۰ درصد از حملات وب شامل نوعی از Injection هستند که XSS سهم بزرگی از آن را به خود اختصاص می‌دهد. طبق آمار HackerOne، XSS یکی از پرگزارش‌ترین آسیب‌پذیری‌ها در برنامه‌های Bug Bounty است و در سه سال گذشته، بیش از ۳۰,۰۰۰ گزارش XSS ثبت شده است. این آمارها نشان می‌دهد که با وجود پیشرفت‌های امنیتی، XSS همچنان یک تهدید جدی است. اگر با مفاهیم پایه‌ای امنیت وب آشنا نیستید، پیشنهاد می‌کنم ابتدا امنیت وب چیست و استانداردهای امنیت وب را مطالعه کنید.

XSS در یک تعریف دقیق

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

نکته حیاتی که XSS را از سایر حملات متمایز می‌کند، سطح دسترسی کد تزریق‌شده است. کد XSS در مرورگر قربانی اجرا می‌شود و بنابراین به کوکی‌ها، LocalStorage، SessionStorage و هر چیزی که صفحه به آن دسترسی دارد، دسترسی دارد. همچنین می‌تواند درخواست‌های HTTP با هویت قربانی ارسال کند، محتوای صفحه را تغییر دهد، یا کاربر را به صفحات فیشینگ هدایت کند. این سطح دسترسی، XSS را به یکی از خطرناک‌ترین آسیب‌پذیری‌های وب تبدیل کرده است.

XSS را نباید با CSRF (Cross-Site Request Forgery) اشتباه گرفت. در CSRF، مهاجم کاربر را مجبور می‌کند یک درخواست ناخواسته به سرور بفرستد، اما کدی در مرورگر قربانی اجرا نمی‌شود. در XSS، کد مهاجم مستقیماً در مرورگر قربانی اجرا می‌شود. این تفکیک، در CSRF چیست و چگونه از آن جلوگیری کنیم به تفصیل بررسی شده است. همچنین اگر با SQL Injection آشنا نیستید، SQL Injection و راه‌های جلوگیری دیدگاه مکملی ارائه می‌دهد.

XSS یک حمله نیست؛ یک خانواده از حملات است. آنچه در همه آنها مشترک است، این است: کد مهاجم در مرورگر قربانی، با اعتمادِ صفحۀ شما، اجرا می‌شود.

سه نوع اصلی XSS

XSS بر اساس مکانیزم تزریق کد و مکان ذخیره‌سازی آن، به سه دسته اصلی تقسیم می‌شود: Reflected، Stored و DOM-based. هر دسته، مکانیزم تزریق متفاوتی دارد و برای مقابله با آن، رویکرد متفاوتی نیاز است.

نوعنقطه تزریقمدت اثرمخاطب
ReflectedURL Parametersموقتکاربر قربانی
StoredDatabaseدائمیهمه بازدیدکنندگان
DOM-basedClient-Side JavaScriptموقتکاربر قربانی

تفاوت این سه، در سطح ریسک و رویکرد مقابله است. Reflected XSS برای قربانی‌گیری نیاز به فریب کاربر دارد (مثلاً از طریق ایمیل یا پیام). Stored XSS بدون نیاز به فریب کاربر اجرا می‌شود و همه بازدیدکنندگان یک صفحه را هدف قرار می‌دهد. DOM-based XSS در سمت کلاینت اجرا می‌شود و اغلب توسط ابزارهای امنیتی سنتی شناسایی نمی‌شود. اگر با انواع آسیب‌پذیری‌های وب آشنا نیستید، انواع آسیب‌پذیری‌های رایج وب و آسیب‌پذیری وب چیست را مطالعه کنید.

Reflected XSS و مکانیزم آن

Reflected XSS زمانی رخ می‌دهد که برنامه، ورودی کاربر را بدون پاک‌سازی در پاسخ فوری بازتاب می‌دهد. مکانیزم ساده است: مهاجم یک URL با پارامتر مخرب می‌سازد، قربانی را به کلیک روی آن ترغیب می‌کند، و سرور بدون پاک‌سازی، پارامتر را در HTML پاسخ درج می‌کند.

نمونه یک URL آسیب‌پذیر:

https://example.com/search?q=";

// خوب: Escape در URL Context
echo "link";

در وردپرس، توابع اختصاصی برای هر Context وجود دارد:

// HTML Body
echo esc_html($userInput);

// HTML Attribute
echo esc_attr($userInput);

// URL
echo esc_url($userInput);

// JavaScript
echo esc_js($userInput);

// HTML با محدودیت Tag
echo wp_kses_post($userInput);

نکات مهم درباره Escape: اول، Escape باید در زمان Output انجام شود، نه زمان Input. اگر در زمان Input Escape کنید، ممکن است داده برای مصارف دیگر (مثل نمایش در گزارش یا ارسال در API) خراب شود. دوم، Escape باید بر اساس Context باشد، نه یکسان برای همه. سوم، برای داده‌ای که نیاز به HTML دارد (مثل کامنت‌های پرمحتوا)، از Allowlist-based Sanitizer استفاده کنید، نه Escape. اگر با PHP و وردپرس کار می‌کنید، نوشتن کد PHP امن برای وردپرس و پاک‌سازی داده‌ها در کدنویسی وردپرس را مطالعه کنید.

Escape درست، مانند پوشیدن کمربند ایمنی است: وقتی اتفاقی نمی‌افتد، متوجه نمی‌شوید؛ وقتی اتفاق می‌افتد، جان شما را نجات می‌دهد. و مثل کمربند، باید همیشه و درست بسته شود.

Input Sanitization و Validation

Input Sanitization و Validation دو مکانیزم مکمل برای مقابله با XSS هستند. Validation، بررسی می‌کند که ورودی با فرمت مورد انتظار مطابقت دارد یا نه. Sanitization، کاراکترهای خطرناک را از ورودی حذف یا بی‌خطر می‌کند. برای مقابله مؤثر با XSS، هر دو مکانیزم لازم است.

Validation شامل: بررسی طول، بررسی نوع (عدد، ایمیل، URL)، بررسی الگو (Regex)، و بررسی مجموعه مجاز. مثلاً اگر یک فیلد باید فقط عدد باشد، ورودی غیرعددی باید رد شود:

$age = filter_var($_POST["age"], FILTER_VALIDATE_INT);
if ($age === false) {
    throw new InvalidArgumentException("سن باید عدد باشد");
}

Sanitization شامل حذف کاراکترهای خطرناک یا Escape آنها است. برای HTML، کتابخانه‌ای مثل HTMLPurifier یا در وردپرس، wp_kses استفاده می‌شود:

// فقط Tags خاص مجاز است
$allowed = [
    "a" => ["href" => [], "title" => []],
    "strong" => [],
    "em" => [],
    "br" => [],
];
$clean = wp_kses($userInput, $allowed);

// برای HTML کامل با محدودیت‌های بیشتر
$clean = wp_kses_post($userInput);

نکات مهم درباره Sanitization: اول، Sanitization بدون Allowlist امن نیست. هر رویکرد Blacklist-based را می‌توان با تکنیک‌های Encoding دور زد. دوم، Sanitization باید بر اساس Context باشد. Sanitizer HTML، برای JSON یا SQL مناسب نیست. سوم، از کتابخانه‌های بالغ استفاده کنید، نه پیاده‌سازی سفارشی. چهارم، Sanitization را در زمان Input انجام دهید تا داده در دیتابیس تمیز ذخیره شود. اگر با اعتبارسنجی داده آشنا نیستید، اعتبارسنجی داده‌ها در کدنویسی وردپرس و پاک‌سازی داده‌ها در کدنویسی وردپرس را بخوانید.

Content Security Policy به عنوان لایه دفاعی

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

یک سیاست CSP امن:

Content-Security-Policy:
  default-src "self";
  script-src "self" "nonce-abc123";
  style-src "self" "unsafe-inline";
  img-src "self" data: https:;
  connect-src "self" https://api.example.com;
  object-src "none";
  base-uri "self";
  frame-ancestors "none"

نکات کلیدی در این سیاست: اول، default-src "self" به این معناست که به طور پیش‌فرض، همه منابع فقط از دامنه خود سایت بارگذاری شوند. دوم، script-src "self" "nonce-abc123" به این معناست که فقط اسکریپت‌های دامنه خود سایت و اسکریپت‌هایی که nonce مشخصی دارند، اجرا شوند. سوم، object-src "none" جلوی بارگذاری پلاگین‌هایی مثل Flash و Java را می‌گیرد. چهارم، frame-ancestors "none" جلوی Clickjacking را می‌گیرد.

رویکرد Nonce-based CSP بهترین تعادل بین امنیت و کاربردی بودن است. Nonce یک رشته تصادفی است که در هر رکورد تولید می‌شود و در هدر CSP و در tag script قرار می‌گیرد. اگر کد XSS تلاش کند اسکریپت اضافه کند، چون nonce ندارد، مرورگر آن را اجرا نمی‌کند.

نکات مهم در پیاده‌سازی CSP: اول، در حالت Report-Only شروع کنید تا نقض‌ها را ببینید. دوم، از nonce یا hash برای اسکریپت‌های inline استفاده کنید، نه 'unsafe-inline'. سوم، report-uri یا report-to را برای دریافت گزارش نقض تنظیم کنید. چهارم، CSP را در چرخه CI/CD قرار دهید تا هر تغییر اعتبارسنجی شود. مباحث بیشتر در راهنمای هدرهای امنیتی HTTP و استانداردهای امنیت وب آمده است.

Trusted Types و آینده مقابله با XSS

Trusted Types یک API جدید مرورگر است که برای مقابله با DOM-based XSS طراحی شده. در سال ۲۰۲۶، Trusted Types در همه مرورگرهای مدرن پشتیبانی می‌شود و به تدریج به یک استاندارد تبدیل می‌شود. این API، راه‌حل بلندمدت‌تری برای مشکل XSS ارائه می‌دهد.

مکانیزم Trusted Types: با اعمال سیاست Trusted Types از طریق CSP، مرورگر جلوی ارسال داده خام به Sink‌های خطرناک مثل innerHTML و eval را می‌گیرد. به جای آن، داده باید در قالب یک "Trusted Value" (که فقط توسط کد معتبر تولید می‌شود) ارسال شود.

// هدر CSP برای فعال‌سازی Trusted Types
Content-Security-Policy: require-trusted-types-for "script";

// در JavaScript، یک سیاست تعریف کنید
const policy = trustedTypes.createPolicy("default", {
  createHTML: (input) => DOMPurify.sanitize(input)
});

// استفاده از policy برای تولید مقدار معتبر
element.innerHTML = policy.createHTML(userInput);

مزایای Trusted Types: اول، جلوگیری از DOM-based XSS در سطح API، نه در سطح کد. دوم، شناسایی خودکار Sink‌های خطرناک. سوم، امکان استفاده از Sanitizerهای امن (مثل DOMPurify) به صورت یکپارچه. چهارم، استانداردسازی شده و در حال گسترش است.

نکات مهم درباره Trusted Types: اول، در حالت Report-Only شروع کنید. دوم، از یک Default Policy استفاده کنید که همه کدها را پوشش دهد. سوم، در CI/CD، Trusted Types را در تست‌ها اعتبارسنجی کنید. چهارم، در کدهای قدیمی که از کتابخانه‌های شخص ثالث استفاده می‌کنند، ممکن است نیاز به اصلاحات داشته باشند. اگر با CSP و هدرهای امنیتی آشنا نیستید، راهنمای هدرهای امنیتی HTTP را مطالعه کنید.

محافظت از کوکی‌ها و Session

حتی با همه لایه‌های دفاعی، اگر XSS رخ دهد، محافظت از کوکی‌ها و Session می‌تواند خسارت را محدود کند. سه مکانیزم اصلی: HttpOnly، Secure و SameSite.

HttpOnly: صفت HttpOnly جلوی دسترسی JavaScript به کوکی را می‌گیرد. اگر XSS رخ دهد، نمی‌تواند کوکی را بخواند و به سرور مهاجم بفرستد:

Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Strict; Path=/

Secure: صفت Secure جلوی ارسال کوکی در درخواست‌های HTTP (غیر از HTTPS) را می‌گیرد. بدون این صفت، کوکی روی شبکه قابل شنود است.

SameSite: سه مقدار: Strict (بالاترین امنیت، کوکی فقط در درخواست‌های هم‌دامنه ارسال می‌شود)، Lax (تعادل، در navigation top-level و درخواست‌های هم‌دامنه)، None (بدون محدودیت، نیاز به Secure). برای کوکی‌های حساس مثل session_id، همیشه Strict یا Lax استفاده کنید.

نکات مهم درباره محافظت از کوکی: اول، همه کوکی‌های حساس باید HttpOnly، Secure و SameSite داشته باشند. دوم، در PHP، session_set_cookie_params یا session.cookie_httponly = 1 در php.ini استفاده کنید. سوم، در وردپرس، افزونه‌های امنیتی این تنظیمات را خودکار اعمال می‌کنند. چهارم، در چرخه CI/CD، پیکربندی کوکی‌ها را اعتبارسنجی کنید. اگر با امنیت وردپرس کار می‌کنید، بهترین افزونه‌های امنیتی وردپرس و مقایسه افزونه‌های امنیتی وردپرس را مطالعه کنید.

تست و شناسایی XSS

تست و شناسایی XSS بخشی جدانشدنی از استراتژی مقابله است. سه رویکرد اصلی: تست دستی، تست خودکار و Bug Bounty.

تست دستی: ابتدا، در همه فیلدهای ورودی payload‌های ساده مثل <script>alert(1)</script> را امتحان کنید. سپس با payload‌های پیچیده‌تر مثل <img src=x onerror=alert(1)> یا تکنیک‌های Encoding مثل %3Cscript%3E، تست کنید. مکان‌های تست: فرم‌ها، پارامترهای URL، فیلدهای Comment، پروفایل کاربری، و ردیف‌های دیتابیس که در پنل ادمین نمایش داده می‌شوند.

تست خودکار: ابزارهایی مثل OWASP ZAP، Burp Suite Scanner، Acunetix و Nessus، XSS را به طور خودکار شناسایی می‌کنند. این ابزارها صفحه را خزش می‌کنند و در همه ورودی‌ها payload ارسال می‌کنند. اما این ابزارها نه‌همه XSS را شناسایی می‌کنند و نه همه هشدارها را درست تشخیص می‌دهند. یک ابزار خودکار باید با ابزار دستی ترکیب شود.

Bug Bounty: برای سایت‌های بزرگ، برنامه Bug Bounty یکی از مؤثرترین راه‌های شناسایی XSS است. طبق آمار HackerOne، XSS یکی از پرگزارش‌ترین آسیب‌پذیری‌هاست. اگر پروژه شما عمومی است، یک برنامه Bug Bounty روی پلتفرم‌هایی مثل HackerOne، Bugcrowd یا YesWeHack راه‌اندازی کنید.

نکات مهم در تست XSS: اول، در محیط staging تست کنید، نه production. دوم، از Payload List‌هایی مثل PayloadsAllTheThings استفاده کنید. سوم، خروجی Payload را در همه Contextها (HTML Body، Attribute، JavaScript، URL) تست کنید. چهارم، به False Positive‌ها توجه کنید و آنها را با تست دستی تایید کنید. اگر با تست امنیت وب آشنا نیستید، تست امنیت وب‌سایت و اسکنرهای آسیب‌پذیری وب را مطالعه کنید.

پرسش‌های پرتکرار درباره XSS

آیا استفاده از WAF جلوی همه XSS را می‌گیرد؟ خیر. WAF یک لایه دفاعی است، اما جایگزین کد امن نیست. برخی payload‌های پیچیده می‌توانند WAF را دور بزنند. XSS باید در سطح کد (با Escape و Sanitization) حل شود، و WAF یک لایه اضافی است.

آیا فریمورک‌هایی مثل React و Vue جلوی XSS را می‌گیرند؟ تا حد زیادی، بله. React و Vue از HTML Escape پیش‌فرض استفاده می‌کنند و داده را به صورت متن نمایش می‌دهند. اما اگر از dangerouslySetInnerHTML (در React) یا v-html (در Vue) استفاده کنید، محافظت پیش‌فرض از بین می‌رود و باید خودتان Sanitize کنید.

آیا Sanitization در زمان Input کافی است؟ خیر. Sanitization در زمان Input برای ذخیره داده تمیز در دیتابیس مفید است، اما Escape در زمان Output لازم است. دلیل این است که داده ذخیره‌شده ممکن است در Contextهای مختلف استفاده شود، و هر Context، Escape متفاوتی نیاز دارد.

آیا CSP می‌تواند همه XSS را مسدود کند؟ CSP بخش بزرگی از XSS را مسدود می‌کند، اما نه همه. برای مثال، DOM-based XSS که از کد خود سایت استفاده می‌کند، اگر CSP به درستی تنظیم نشده باشد، می‌تواند رخ دهد. Trusted Types مکمل CSP است.

آیا باید از Blacklist برای Sanitization استفاده کنم؟ خیر. Blacklist (حذف کاراکترهای خاص مثل <script>) را می‌توان با تکنیک‌های Encoding دور زد. رویکرد امن، Allowlist است: فقط کاراکترها یا Tagهای مجاز را قبول کن و بقیه را Escape یا حذف کن.

چگونه بفهمم سایتم آسیب‌پذیر XSS است؟ از ابزارهای تست خودکار مثل OWASP ZAP و Burp Suite استفاده کنید. اما این ابزارها نه همه XSS را شناسایی می‌کنند و نه همه هشدارها را درست تشخیص می‌دهند. بهترین رویکرد، ترکیب ابزار خودکار با تست دستی و در صورت امکان، Bug Bounty است.

آیا وردپرس در برابر XSS محافظت می‌کند؟ تا حدی، بله. هسته وردپرس از توابع Escape مثل esc_html و esc_attr استفاده می‌کند. اما این محافظت به قالب‌ها و افزونه‌ها منتقل نمی‌شود. قالب‌ها و افزونه‌های ضعیف می‌توانند XSS را وارد کنند. اگر با وردپرس کار می‌کنید، راهنمای امنیت وردپرس و اشتباهات رایج امنیت وردپرس را بخوانید.

آنچه باید با خود ببرید

XSS یکی از پرخطرترین آسیب‌پذیری‌های وب است که به مهاجم اجازه می‌دهد کد جاوااسکریپت در مرورگر قربانی اجرا کند. سه نوع اصلی XSS وجود دارد: Reflected (کوتاه‌مدت، نیاز به فریب قربانی)، Stored (دائمی، هدف همه بازدیدکنندگان) و DOM-based (سمت کلاینت، اجرا در طول JavaScript).

هفت اصل کلیدی برای مقابله با XSS:

  1. Context-Aware Output Escaping، مؤثرترین راه مقابله است.
  2. Input Sanitization با Allowlist، پایه ذخیره داده تمیز در دیتابیس است.
  3. CSP به عنوان لایه دفاعی در مرورگر عمل می‌کند.
  4. Trusted Types راه‌حل بلندمدت برای DOM-based XSS است.
  5. HttpOnly، Secure و SameSite، کوکی‌ها را محافظت می‌کنند.
  6. تست خودکار و دستی، تکمیل‌کننده یکدیگرند.
  7. Defense in Depth، ترکیب چند لایه دفاعی، رویکرد پیشنهادی است.

قدم عملی امروز: در کد پروژه خود، همه نقاطی که داده کاربر را نمایش می‌دهند را بررسی کنید. آیا از Escape در Context درست استفاده می‌کنید؟ آیا در همه Sink‌های خطرناک مثل innerHTML و eval، محافظت لازم وجود دارد؟ آیا CSP فعال است؟ این سه بررسی، نیمی از مسیر مقابله با XSS را پوشش می‌دهد. اگر می‌خواهید عمیق‌تر شوید، استانداردهای امنیت وب و انواع آسیب‌پذیری‌های رایج وب را مطالعه کنید.

اگر تجربه‌ای در شناسایی یا مقابله با XSS در پروژه‌های واقعی داشتید — به‌خصوص اگر با یک Stored XSS پیچیده مواجه شده‌اید یا یک تکنیک دور زدن WAF را کشف کرده‌اید — در دیدگاه‌ها بنویسید. این تجربه‌ها برای خواننده‌های بعدی از هر مقاله تئوریک ارزشمندتر است. 🛡️