تأمين الموارد الفرعية الخارجية باستخدام "سلامة الموارد الفرعية" و"مشاركة الموارد المشتركة المنشأ" و"سياسة المُحيل"

عند تحميل نصوص برمجية أو أوراق أنماط أو خطوط أو صور من شبكات توصيل محتوى (CDN) تابعة لجهات خارجية أو مصادر خارجية، يجب التحقّق من سلامة تشفيرها باستخدام ميزة "سلامة الموارد الفرعية" (integrity في HTML وintegrity() في CSS url())، وضبط معالجة بيانات الاعتماد المتعدّدة المصادر (crossorigin وcross-origin())، وحظر عناوين Referer الصادرة (referrerpolicy وreferrer-policy()). للاطّلاع على "سياسة أمان المحتوى" على مستوى المستند والقوائم المسموح بها للاتصالات الصادرة، يُرجى مراجعة أمان الويب وحظر الاتصالات الصادرة بالشبكة باستخدام القوائم المسموح بها للاتصالات.

1. إنشاء ملخّصات لسلامة التشفير لمواد العرض الثابتة

تضمن ميزة "سلامة الموارد الفرعية" (SRI) أن ينفّذ المتصفّح أو يعرض مادة عرض خارجية فقط إذا كانت وحدات البايت الخاصة بها تتطابق مع التجزئة التشفيرية التي يعلن عنها تطبيقك.

  • احتساب التجزئات في وقت الإنشاء أو الإصدار، وليس من الردود المباشرة غير الموثوق بها: يمكنك إنشاء تجزئات السلامة أثناء خطوة الإنشاء أو الإصدار من وحدات بايت مواد العرض المحلية المعروفة بأنّها جيدة (أو نسخ التجزئة الرسمية التي نشرها مؤلف الحزمة) وترميزها بشكل ثابت في HTML أو CSS أو JavaScript. لا تجلب أبدًا عنوان URL تابعًا لجهة خارجية في المتصفّح في وقت التشغيل لحساب تجزئة integrity الخاصة به، والتي تثق في أي وحدات بايت يعرضها الخادم البعيد وتتغلب على SRI.
  • استخدام sha384- تلقائيًا: يمكنك تنسيق البيانات الوصفية الخاصة بالسلامة على أنّها <algorithm>-<base64-digest> باستخدام sha384- (الإعداد التلقائي المقترَح) أو sha512- أو sha256-.
  • تثبيت الموارد الفرعية الخارجية على عناوين URL غير قابلة للتغيير ومحدّدة الإصدار: يجب دائمًا الإشارة إلى مواد العرض التابعة لجهات خارجية باستخدام عناوين URL غير قابلة للتغيير أو محدّدة الإصدار أو موجَّهة حسب المحتوى (مثل https://cdn.example.com/ui@2.4.0/app.min.js) بدلاً من عناوين URL العائمة القابلة للتغيير (latest/app.min.js)، والتي تعرّض موقعك الإلكتروني لتغييرات غير معلنة في سلسلة الإمداد وتؤدي إلى تعطُّل SRI فور تحديث الملف البعيد. إذا كان عليك تحميل نص برمجي تابع لجهة خارجية قابل للتغيير ويتم تحديثه في مكانه (مثل علامة إحصاءات المورّد)، يمكنك حذف integrity في عنوان URL هذا وتقييده باستخدام "سياسة أمان المحتوى" (strict-dynamic / الأرقام المستخدمة لمرة واحدة) أو وضع الحماية في iframe بدلاً من ذلك.
  • الفصل بين الملخّصات المتعددة بمسافة عندما تعرض نقطة نهاية حمولات متعددة صالحة: جميع المتصفحات التي تتيح SRI تنفّذ sha256- وsha384- وsha512-. إذا كان عنوان URL قد يعرض بشكل مشروع إحدى الحمولات المتعددة المعروفة بأنّها جيدة (مثل أثناء اختبار A/B أو طرح مرحلي لشبكة توصيل المحتوى أو إصدار تم التفاوض على محتواه)، يمكنك إدراج ملخّصات متعددة للخوارزمية نفسها مفصولة بمسافة بيضاء (على سبيل المثال، 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)}`;
}

2. تأمين نصوص HTML البرمجية وأوراق الأنماط وعمليات التحميل المُسبق

اضبط عنوان استجابة HTTP‏ Referrer-Policy: strict-origin-when-cross-origin على مستوى المستند (أو ضمِّن <meta name="referrer" content="strict-origin-when-cross-origin"> في <head>) كإعداد تلقائي أساسي، بحيث يستمر أي مورد فرعي أو url() CSS يحذف معدِّل المُحيل لكل طلب في إزالة المسار الحسّاس ومعلمات طلب البحث في الطلبات الواردة من عدّة مصادر.

بعد ذلك، في عناصر <script> و<link rel="stylesheet"> و<link rel="preload"> و<link rel="modulepreload"> الخارجية، اضبط دائمًا integrity وcrossorigin وreferrerpolicy معًا:

  • integrity="sha384-...": يحظر تنفيذ المورد أو تطبيقه إذا كانت وحدات البايت في الاستجابة لا تتطابق مع الملخّص المتوقّع (بالنسبة إلى <link rel="stylesheet"> المحظور، يظل linkElement.sheet null ويتم تنشيط حدث error).
  • ‫crossorigin="anonymous" (الإعداد التلقائي المقترَح لمواد عرض شبكة توصيل المحتوى العامة) أو crossorigin="use-credentials": مطلوبان عند ضبط integrity على مورد متعدّد المصادر. يؤدي حذف crossorigin إلى جلب المتصفّح للمورد في وضع no-cors (ما يؤدي إلى استجابة مبهمة لا يمكن التحقّق منها باستخدام التجزئة) وحظر المورد.
  • ‫referrerpolicy="no-referrer" (الإعداد التلقائي المقترَح لشبكات توصيل المحتوى التابعة لجهات خارجية) أو 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',
});

3- تأمين صور 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 الذي تم جلبه مقابل الملخّص المشفّر المتوقّع قبل عرضه أو تطبيقه.

اتّبِع قواعد الأمان وبنية CSS التالية عند إنشاء معدِّلات url():

  • اقتباس سلسلة عنوان URL داخل url("..."): لا تكون معدِّلات الطلبات صالحة إلا عندما يكون عنوان URL داخل url(...) عبارة عن <string> مقتبس (لا يمكن لعناوين URL غير المقتبسة استخدام المعدِّلات).
  • تحديد كل نوع معدِّل مرة واحدة على الأكثر لكل 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 (ما يؤدي إلى استجابة مبهمة لا يمكن التحقّق من تجزئتها)، وتستخدم السمة filter لملفات SVG الخارجية تلقائيًا وضع same-origin (ما يؤدي إلى حظر عناوين URL المتعددة المصادر بشكل كامل). يضمن تضمين 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. يُفضَّل استخدام HTML <link rel="stylesheet"> بدلاً من CSS @import عند الحاجة إلى استخدام SRI على مستوى المتصفّحات: نظرًا لأنّ 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"> من الخطوة 2: في المتصفحات التي لا تتيح استخدام 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. Feature-detect CSS url() modifier support in JavaScript with 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 };
}