برای جلوگیری سریع از XSS، تنظیم Content Security Policy باید به این صورت انجام شود: ابتدا آن را در حالت Report-Only فعال کنید، script-src را فقط به self و اسکریپت‌هایی با nonce یا hash محدود کنید، object-src را none بگذارید، frame-ancestors را برای مقابله با کلیک‌جکینگ تنظیم کنید، منابع ضروری مثل CDNهای مشخص را به صورت دقیق اجازه دهید، تخلفات را در کنسول بررسی کنید و در نهایت سیاست را به حالت enforce تغییر دهید.

CSP دقیقا چه می‌کند و چرا برای XSS حیاتی است؟

Content Security Policy یک هدر امنیتی است که تعیین می‌کند مرورگر چه منابعی (اسکریپت، استایل، تصویر، فونت و…) را از کجا و با چه شرایطی بارگذاری کند. با محدود کردن script-src به منابع مطمئن و استفاده از nonce یا hash برای اسکریپت‌ها، تزریق اسکریپت مهاجم عملا بی‌اثر می‌شود؛ حتی اگر کد مخرب در HTML شما قرار بگیرد، مرورگر آن را اجرا نخواهد کرد.

مسیر اجرای سریع: از گزارش‌گیری تا اجرا

۱) شناسایی منابع واقعاً لازم

فهرستی از دامنه‌هایی که اسکریپت، استایل، تصویر و فونت از آن‌ها بارگذاری می‌کنید تهیه کنید (دامنه خودتان، CDNها، سرویس آنالیتیکس و…). هر منبع غیرضروری را حذف کنید.

۲) شروع در حالت Report-Only

سیاست اولیه را در حالت گزارش‌گیری اعمال کنید تا چیزی نشکند و خطاها را ببینید.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self'; font-src 'self' data:; form-action 'self'

سپس صفحات را بررسی کنید و پیام‌های نقض CSP را در کنسول مرورگر ببینید.

۳) حذف اسکریپت‌های inline یا امن‌سازی با nonce/hash

  • بهترین راه: تمام اسکریپت‌های inline را به فایل منتقل کنید و فقط از فایل‌های مجاز استفاده کنید.
  • اگر مجبور به inline هستید، برای هر پاسخ یک nonce تصادفی بسازید و روی script-src و روی تگ <script> قرار دهید.
  • برای اسکریپت‌های کوچک ثابت می‌توانید از hash (مثلا sha256) همان محتوا استفاده کنید.

۴) اجازه‌های دقیق برای منابع ثالث

به جای باز گذاشتن https: یا * فقط دامنه‌های واقعا لازم را به هر directive اضافه کنید. برای مثال فقط cdn.example.com را به script-src یا style-src اضافه کنید.

۵) اعمال نهایی (enforce)

وقتی خطاهای Report-Only برطرف شد، هدر را به Content-Security-Policy تغییر دهید تا مرورگر تخلفات را مسدود کند.

پیاده‌سازی در سرورهای متداول

Nginx

هدر را در بلاک server برای پاسخ‌های HTML اضافه کنید:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self'; font-src 'self' data:; form-action 'self'; upgrade-insecure-requests" always;

نکته: از یک nonce واقعی و تصادفی برای هر پاسخ استفاده کنید؛ $request_id فقط نمونه است. اگر پشت CDN هستید مراقب کش شدن پاسخ‌های HTML با nonce ثابت نباشید؛ HTML باید بدون کش یا با vary مناسب تحویل شود.

Apache (httpd)

Header set Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self'; font-src 'self' data:; form-action 'self'"

برای nonce باید از ماژول‌ها/اکستنشن‌های تولید مقدار تصادفی یا درج از سمت برنامه استفاده کنید.

Node.js (Express) با Helmet

Helmet پیکربندی CSP را ساده می‌کند. یک nonce در هر درخواست بسازید و به تگ‌های <script> اضافه کنید.

const express = require('express');
const crypto = require('crypto');
const helmet = require('helmet');

const app = express();

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  helmet.contentSecurityPolicy({
    useDefaults: false,
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", `'nonce-${res.locals.cspNonce}'`],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
      frameAncestors: ["'none'"],
      imgSrc: ["'self'", "data:", "https:"],
      styleSrc: ["'self'"],
      fontSrc: ["'self'", "data:"],
      formAction: ["'self'"],
      upgradeInsecureRequests: []
    }
  })(req, res, next);
});

app.get('/', (req, res) => {
  res.send(`<!doctype html>
  <html><head><meta charset="utf-8"></head>
  <body>
    <script nonce="${res.locals.cspNonce}">console.log('CSP OK')</script>
  </body></html>`);
});

app.listen(3000);

خروجی را در DevTools بررسی کنید: تگ اسکریپت باید nonce داشته باشد و در Network هدر CSP دیده شود.

WordPress (افزودن هدر و nonce)

یک nonce بسازید، در هدر CSP استفاده کنید و به تگ‌های اسکریپت اضافه کنید. نمونه زیر برای functions.php است و صرفا آموزشی است؛ متناسب با قالب/افزونه‌های خود تنظیم کنید.

<?php
add_action('send_headers', function () {
  if (!is_admin()) {
    if (!isset($GLOBALS['csp_nonce'])) {
      $GLOBALS['csp_nonce'] = base64_encode(random_bytes(16));
    }
    header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{$GLOBALS['csp_nonce']}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; style-src 'self'; font-src 'self' data:; form-action 'self'");
  }
});

add_filter('script_loader_tag', function ($tag, $handle, $src) {
  if (is_admin()) return $tag;
  if (!isset($GLOBALS['csp_nonce'])) return $tag;
  // به تگ اسکریپت nonce اضافه می‌کند
  $tag = preg_replace('/<script /', '<script nonce="' . esc_attr($GLOBALS['csp_nonce']) . '" ', $tag, 1);
  return $tag;
}, 10, 3);
?>

نکته: اسکریپت‌های inline یا درج‌شده توسط افزونه‌ها ممکن است نیاز به بازنگری داشته باشند. از افزودن ‘unsafe-inline’ خودداری کنید و در صورت نیاز، محتوا را به فایل منتقل یا از nonce/hash استفاده کنید.

تست و پایش نتیجه

  • DevTools: در تب Console پیام‌های Violated Content Security Policy را ببینید.
  • بررسی هدر: با curl -I مطمئن شوید هدر ارسالی است.
curl -I https://example.com | grep -i content-security-policy

برای دریافت گزارش خودکار، یک endpoint گزارش بسازید و از report-uri استفاده کنید.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report
app.post('/csp-report', express.json({ type: ['application/csp-report','application/json'] }), (req, res) => {
  console.log('CSP report:', req.body);
  res.sendStatus(204);
});

پس از رفع همه اخطارها، هدر را از Report-Only به Content-Security-Policy تغییر دهید.

سیاست پیشنهادی نمونه (قابل سفارشی‌سازی)

این الگو را متناسب با نیاز خود محدودتر یا بازتر کنید. برای nonce مقدار تصادفی هر پاسخ را بگذارید.

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' 'nonce-{{RANDOM_NONCE}}';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  img-src 'self' data: https:;
  style-src 'self';
  font-src 'self' data:;
  connect-src 'self';
  form-action 'self';
  upgrade-insecure-requests;

اگر از CDN اسکریپت استفاده می‌کنید، همان دامنه‌ها را به script-src اضافه کنید. اگر اسکریپت‌های قابل اعتماد شما اسکریپت‌های دیگری را دینامیک بارگذاری می‌کنند، می‌توانید در سناریوهای پیشرفته از ‘strict-dynamic’ همراه با nonce استفاده کنید؛ اما این گزینه نیاز به دقت و آزمون بیشتر دارد.

اشتباه‌های رایج و راهکارها

  • استفاده از ‘unsafe-inline’ یا ‘unsafe-eval’: این‌ها امنیت CSP را تضعیف می‌کنند. به جای آن از nonce/hash و بازنویسی کد استفاده کنید.
  • باز گذاشتن wildcard مانند * یا https:: این کار سطح حمله را وسیع می‌کند. فقط دامنه‌های دقیقا لازم را مجاز کنید.
  • فراموش کردن frame-ancestors: نبود این directive می‌تواند به کلیک‌جکینگ منجر شود. اگر نیاز به iframe ندارید، آن را ‘none’ بگذارید.
  • کش اشتباه HTML با nonce: اگر HTML با nonce در CDN کش شود، همان nonce برای همه کاربران تکرار می‌شود و اسکریپت‌ها اجرا نمی‌شوند. برای صفحات HTML، کش را غیرفعال یا طوری تنظیم کنید که nonce برای هر پاسخ یکتا باشد.
  • نادیده گرفتن reportها: گزارش‌های Report-Only سرنخ‌های دقیقی از منابعی می‌دهند که باید مجاز یا حذف شوند. آن‌ها را بررسی کنید.

چک لیست اجرایی کوتاه

  1. فهرست دامنه‌های منابع را تهیه کنید.
  2. CSP را با Report-Only فعال کنید.
  3. اسکریپت‌های inline را حذف یا با nonce/hash امن کنید.
  4. فقط دامنه‌های لازم را به هر directive اضافه کنید.
  5. گزارش‌ها و کنسول را بررسی کنید.
  6. به حالت enforce تغییر دهید و مانیتورینگ را ادامه دهید.

سوالات متداول

nonce بهتر است یا hash؟

اگر اسکریپت inline شما پویا یا متغیر است، nonce مناسب‌تر است چون در هر پاسخ یکتا است. اگر اسکریپت inline شما کوچک و ثابت است، hash ساده‌تر است. در هر دو حالت از ‘unsafe-inline’ پرهیز کنید.

آیا متاتگ CSP کافی است؟

خیر، توصیه اصلی ارسال هدر HTTP است. متاتگ (<meta http-equiv=”Content-Security-Policy”>) فقط وقتی خوانده می‌شود که HTML بدون اسکریپت‌های قبل از آن لود شود و برخی قابلیت‌ها در هدر بهتر و قابل اتکاتر هستند.

آیا CSP جایگزین اعتبارسنجی ورودی و فراردهی خروجی است؟

خیر، CSP یک لایه دفاعی اضافی است. همچنان باید ورودی‌ها را اعتبارسنجی و خروجی‌ها را escape کنید. ترکیب چند لایه دفاع، ریسک XSS را به شکل معناداری کاهش می‌دهد.

گام بعدی چیست؟

از حالت Report-Only شروع کنید، script-src را حداقلی کنید، nonce/hash را پیاده کنید، گزارش‌ها را بخوانید و سپس سیاست را enforce کنید. این مسیر عملی کمترین ریسک اختلال را دارد و بیشترین تاثیر را بر کاهش XSS ایجاد می‌کند.