ایمن‌سازی زیرمنابع خارجی با «یکپارچگی زیرمنبع»، CORS، و «خط‌مشی ارجاع‌دهنده»

هنگام بار کردن دستورگان‌ها، برگه‌های سبک نگارش، قلم‌ها، یا تصاویر از CDNهای طرف سوم یا مبدأهای خارجی، یکپارچگی رمزنگاری آن‌ها را با «یکپارچگی زیرمنبع» (integrity در HTML و integrity() در CSS url()) تأیید کنید، مدیریت اطلاعات کاربری میان‌مبدئی (crossorigin و cross-origin()) را پیکربندی کنید، و سرصفحه‌های Referer خروجی (referrerpolicy و referrer-policy()) را محدود کنید. برای «خط‌مشی امنیتی محتوا» در سطح سند و فهرست‌های مجاز اتصال خروجی، به امنیت وب و محدود کردن اتصال‌های شبکه خروجی با فهرست‌های مجاز اتصال مراجعه کنید.

۱. تولید خلاصه یکپارچگی رمزنگاری برای دارایی‌های تغییرناپذیر

«یکپارچگی زیرمنبع» (SRI) تضمین می‌کند که مرورگر فقط درصورتی دارایی خارجی را اجرا یا ارائه می‌کند که بایت‌های آن با درهم‌ساز رمزنگاری اعلام‌شده توسط برنامه شما مطابقت داشته باشد.

  • خلاصه‌ها را در زمان ساخت یا انتشار محاسبه کنید، هرگز از پاسخ‌های غیرقابل‌اعتماد زنده استفاده نکنید: درطول مرحله ساخت یا انتشار، درهم‌سازی‌های یکپارچگی را از بایت‌های دارایی محلی شناخته‌شده و خوب تولید کنید (یا خلاصه رسمی منتشرشده توسط نویسنده بسته را کپی کنید) و آن‌ها را در HTML،‏ CSS، یا JavaScript خود به‌صورت کد ثابت قرار دهید. هرگز نشانی وب طرف سوم را در مرورگر در زمان اجرا واکشی نکنید تا درهم‌سازی integrity خودش را محاسبه کند، که به هر بایتی که سرور راه دور برمی‌گرداند اعتماد می‌کند و SRI را شکست می‌دهد.
  • استفاده از sha384- به‌طور پیش‌فرض: فراداده یکپارچگی را بااستفاده از sha384- (پیش‌فرض توصیه‌شده)، sha512-، یا sha256- به‌صورت <algorithm>-<base64-digest> قالب‌بندی کنید.
  • سنجاق کردن زیرمنابع خارجی به نشانی‌های وب تغییرناپذیر و نسخه‌دار: همیشه بااستفاده از نشانی‌های وب تغییرناپذیر، نسخه‌سنجاق‌شده، یا محتواآدرس‌دهی‌شده (مثل https://cdn.example.com/ui@2.4.0/app.min.js) به دارایی‌های طرف سوم ارجاع دهید، نه نشانی‌های وب شناور تغییرپذیر (latest/app.min.js) که سایت شما را درمعرض تغییرات زنجیره تأمین بی‌صدا قرار می‌دهند و به‌محض به‌روزرسانی فایل راه دور، «یکپارچگی منبع فرعی» را می‌شکنند. اگر باید دستورگان طرف سوم تغییرپذیری را بار کنید که در محل به‌روز می‌شود (مانند برچسب تجزیه‌وتحلیل فروشنده)، integrity را در آن نشانی وب حذف کنید و به‌جای آن، آن را با «خط‌مشی امنیتی محتوا» (strict-dynamic / nonces) یا جعبه شنی iframe محدود کنید.
  • وقتی یک نقطه پایانی ممکن است چندین بار داده معتبر ارائه دهد، چندین خلاصه را با فاصله از هم جدا کنید: همه مرورگرهایی که از SRI پشتیبانی می‌کنند، sha256-، sha384-، و sha512- را پیاده‌سازی می‌کنند. اگر نشانی وب ممکن است به‌طور قانونی یکی از چندین محموله شناخته‌شده خوب را برگرداند (مثلاً درطول آزمایش A/B، عرضه CDN مرحله‌ای، یا ساخت مذاکره‌شده محتوا)، چندین خلاصه از الگوریتم یکسان را که با فاصله از هم جدا شده‌اند فهرست کنید (برای مثال، integrity="sha384-variantA... sha384-variantB..."). اگر پاسخ با هر خلاصه فهرست‌شده از قوی‌ترین الگوریتم موجود مطابقت داشته باشد، مرورگر آن را می‌پذیرد.
  • ارائه زیرمنابع میان‌مبدئی با سرصفحه‌های CORS: مرورگرها فقط زمانی می‌توانند یکپارچگی پاسخ میان‌مبدئی را تأیید کنند که CORS-خوانا باشد (Access-Control-Allow-Origin: * برای درخواست‌های ناشناس، یا مبدأ دقیق درخواست‌کننده با Access-Control-Allow-Credentials: true برای درخواست‌های دارای اعتبارنامه).
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

۲. اسکریپت‌ها، برگه‌های سبک نگارش، و پیش‌بارگیری‌های HTML ایمن

یک سرایند پاسخ HTTP Referrer-Policy: strict-origin-when-cross-origin در سطح سند تنظیم کنید (یا <meta name="referrer" content="strict-origin-when-cross-origin"> را در <head> بگنجانید) تا به‌عنوان خط مبنای پیش‌فرض عمل کند، به‌طوری‌که هر منبع فرعی یا CSS url() که اصلاح‌کننده ارجاع‌دهنده برای هر درخواست را حذف می‌کند، همچنان پارامترهای پُرسمان و مسیر حساس را در درخواست‌های میان‌مبدئی حذف کند.

سپس، در عناصر خارجی <script>، <link rel="stylesheet">، <link rel="preload">، و <link rel="modulepreload">، همیشه integrity، crossorigin، و referrerpolicy را باهم تنظیم کنید:

  • integrity="sha384-...": اگر بایت‌های پاسخ با خلاصه موردانتظار مطابقت نداشته باشد، اجرای منبع یا برنامه را مسدود می‌کند (برای <link rel="stylesheet"> مسدودشده، linkElement.sheet به‌صورت null باقی می‌ماند و رویداد error فعال می‌شود).
  • crossorigin="anonymous" (پیش‌فرض توصیه‌شده برای دارایی‌های CDN عمومی) یا crossorigin="use-credentials": هرگاه integrity در منبع میان‌مبدئی تنظیم شده باشد، الزامی است. حذف crossorigin باعث می‌شود مرورگر منبع را در حالت no-cors واکشی کند (پاسخی مبهم تولید می‌کند که نمی‌تواند با درهم‌سازی درستی‌سنجی شود) و منبع را مسدود کند.
  • referrerpolicy="no-referrer" (پیش‌فرض توصیه‌شده برای CDNهای طرف سوم) یا referrerpolicy="strict-origin-when-cross-origin": خط‌مشی ارجاع‌دهنده سند را لغو می‌کند تا از نشت اطلاعات مبدأ یا مسیر در سرایند درخواست Referer به میزبان طرف سوم جلوگیری شود.
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

هنگام واکشی برنامه‌ای زیرمنابع خارجی با fetch()، گزینه‌های منطبق integrity،‏ mode،‏ credentials، و referrerPolicy را ارسال کنید (عدم تطابق خلاصه، وعده را با TypeError رد می‌کند):

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

۳. تصاویر CSS، قلم‌ها، و برگه‌های سبک نگارش واردشده را با اصلاح‌کننده‌های url() ایمن کنید

اصلاح‌کننده‌های درخواست CSS url() (cross-origin()، referrer-policy()، و integrity()) حالت واکشی، سرصفحه ارجاع‌دهنده، و درستی‌سنجی «یکپارچگی زیرمنبع» منابع بارشده با CSS را (ازجمله قوانین background-image، content، mask-image، border-image-source، filter، @font-face src، و @import) مستقیماً از CSS کنترل می‌کنند:

تابع اصلاح‌گر نحو آرگومان و مقادیر مجاز هدف
cross-origin(<ident>) کلیدواژه بدون نقل‌قول: anonymous (پیش‌فرض توصیه‌شده) یا use-credentials منبع CSS را بااستفاده از CORS واکشی می‌کند و ارسال شدن یا نشدن اعتبارنامه‌ها را کنترل می‌کند. همیشه در کنار integrity() در منابع CSS میان‌مبدئی گنجانده شود.
referrer-policy(<ident>) کلیدواژه بدون نقل‌قول: no-referrer (پیش‌فرض توصیه‌شده برای دارایی‌های شخص ثالث)، strict-origin-when-cross-origin، origin، same-origin، strict-origin، origin-when-cross-origin، no-referrer-when-downgrade، یا unsafe-url خط‌مشی ارجاع‌دهنده سند را برای این درخواست CSS خاص لغو می‌کند.
integrity("<string>") رشته نقل‌شده: "sha384-<base64>" (یا خلاصه‌های جداشده با فاصله "sha384-... sha384-...") منبع CSS واکشی‌شده را قبل‌از ارائه یا اعمال آن، با خلاصه رمزنگاری موردانتظار درستی‌سنجی می‌کند.

هنگام نوشتن اصلاح‌کننده‌های url()، این قوانین امنیتی و نحو CSS را دنبال کنید:

  • رشته نشانی وب را در url("...") نقل‌قول کنید: اصلاح‌کننده‌های درخواست فقط زمانی معتبر هستند که نشانی وب داخل url(...) نقل‌قول‌شده <string> باشد (نشانی‌های وب نقل‌قول‌نشده نمی‌توانند از اصلاح‌کننده‌ها استفاده کنند).
  • هر نوع اصلاح‌کننده را حداکثر یک‌بار در هر url() مشخص کنید: می‌توانید cross-origin()، referrer-policy()، و integrity() را به هر ترتیبی در url("..." ...) ترکیب کنید، اما تکرار دو بار یک نوع اصلاح‌کننده (مانند دو اصلاح‌کننده integrity()) نامعتبر است. برای ارائه چند درهم‌سازی، آن‌ها را با فاصله در یک رشته integrity("sha384-... sha384-...") از هم جدا کنید.
  • همیشه integrity() میان‌مبدئی را با cross-origin(anonymous) جفت کنید: درحالی‌که @font-face src و mask-image ازقبل به‌طور پیش‌فرض در حالت ناشناس cors واکشی می‌کنند، background-image،‏ content،‏ border-image-source، و @import به‌طور پیش‌فرض در حالت no-cors هستند (که پاسخی مبهم تولید می‌کند که نمی‌تواند درستی‌سنجی درهم‌سازی شود) و SVG خارجی filter به‌طور پیش‌فرض در حالت same-origin است (که نشانی‌های وب میان‌مبدئی را کاملاً مسدود می‌کند). گنجاندن cross-origin(anonymous) (یا cross-origin(use-credentials)) درستی‌سنجی CORS را در همه دارایی‌های CSS تضمین می‌کند.
  • اصلاح‌کننده‌ها را در url("...") در قوانین @import و @font-face قرار دهید: cross-origin()، referrer-policy()، و integrity() را در url("..." ...) نگه دارید، درحالی‌که توصیف‌کننده‌های قلم مانند format("woff2") و tech(...) را خارج از url(...) نگه می‌دارید.
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

راهبردهای جایگزین

اگر هدف «خط پایه» شما هنوز از اصلاح‌کننده‌های درخواست CSS url() (cross-origin()، referrer-policy()، یا integrity()) پشتیبانی نمی‌کند، خط‌مشی‌های سطح سند را با اعلامیه‌های CSS آبشاری ترکیب کنید تا زیرمنابع همچنان کاربردی و محافظت‌شده باقی بمانند:

  1. وقتی SRI بین‌مرورگری لازم است، HTML <link rel="stylesheet"> را به CSS @import ترجیح دهید: چون subresource-integrity و referrer-policy در <link rel="stylesheet"> به‌طور گسترده در همه مرورگرها دردسترس هستند، هرگاه درستی‌سنجی یکپارچگی باید در همه مرورگرها اجرا شود، برگه سبک نگارش خارجی را با <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> به‌جای @import بار کنید.
  2. به سرصفحه Referrer-Policy در سطح سند یا <meta name="referrer"> از مرحله ۲ تکیه کنید: در مرورگرهایی که از referrer-policy() در CSS url() پشتیبانی نمی‌کنند، سرصفحه Referrer-Policy: strict-origin-when-cross-origin در سطح سند یا عنصر <meta name="referrer" content="strict-origin-when-cross-origin"> به‌طور خودکار مسیر حساس و پارامترهای پرسمان را در درخواست‌های CSS میان‌مبدئی حذف می‌کند.
  3. اعلامیه‌های CSS جداگانه را برای ارتقای پیش‌رو آبشاری کنید: مرورگرهایی که اصلاح‌کننده را در url("..." ...) تشخیص نمی‌دهند، کل اعلامیه را در زمان تجزیه CSS کنار می‌گذارند. ابتدا یک url("...") ساده را اعلام کنید، سپس یک اعلامیه cross-origin() و referrer-policy() (برای مرورگرهایی که از این دو اصلاح‌کننده پشتیبانی می‌کنند اما از integrity() پشتیبانی نمی‌کنند)، و سپس اعلامیه کامل شامل integrity("sha384-..."):

    • هرگز url() درستی‌سنجی‌نشده را در همان فهرست @font-face src جداشده با کاما قرار ندهید: نوشتن src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); در یک توصیف‌گر src واحد، «یکپارچگی زیرمنبع» را در زمان اجرا دور می‌زند—اگر CDN قلم دستکاری‌شده‌ای را ارائه دهد که درستی‌سنجی integrity() را رد کند، مرورگر آن را به‌عنوان خطای بارگیری درنظر می‌گیرد و به‌طور خودکار به url("font.woff2") جداشده با کاما دوم برمی‌گردد. استفاده از اعلامیه‌های جداگانه src: تضمین می‌کند که مرورگرهای پشتیبانی‌کننده، اعلامیه درستی‌سنجی‌نشده را در زمان تجزیه CSS بازنویسی کنند و قلم‌های دست‌کاری‌شده را در زمان اجرا مسدود کنند.
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. تشخیص ویژگی پشتیبانی از اصلاح‌کننده CSS url() در جاوا اسکریپت با CSS.supports():

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}