אבטחת משאבי משנה חיצוניים באמצעות Subresource Integrity,‏ CORS ו-Referrer Policy

כשאתם טוענים סקריפטים, גיליונות סגנונות, גופנים או תמונות מרשתות CDN של צד שלישי או ממקורות חיצוניים, אתם צריכים לאמת את התקינות הקריפטוגרפית שלהם באמצעות Subresource Integrity (integrity ב-HTML ו-integrity() ב-CSS url()), להגדיר טיפול בפרטי כניסה ממקורות שונים (crossorigin ו-cross-origin()) ולהגביל כותרות Referer יוצאות (referrerpolicy ו-referrer-policy()). כדי לקבל מידע על Content Security Policy ברמת המסמך ורשימות היתרים של חיבורים יוצאים, קראו את המאמרים אבטחת אינטרנט והגבלת חיבורים יוצאים לרשת באמצעות רשימות היתרים של חיבורים.

1. יצירת תמציות קריפטוגרפיות של תקינות לנכסים שלא משתנים

תקינות משאבי משנה (SRI) מבטיחה שהדפדפן יפעיל או יעבד נכס חיצוני רק אם הבייטים שלו תואמים לגיבוב הקריפטוגרפי שהוצהר על ידי האפליקציה.

  • חישוב תקצירים בזמן ה-build או ההפצה, אף פעם לא מתגובות פעילות לא מהימנות: יוצרים גיבובים של תקינות במהלך שלב ה-build או ההפצה מבייטים של נכסים מקומיים תקינים וידועים (או מעתיקים את התקציר הרשמי שפורסם על ידי מחבר החבילה) ומקודדים אותם בתוך ה-HTML, ה-CSS או ה-JavaScript. אף פעם אל תאחזרו כתובת URL של צד שלישי בדפדפן בזמן הריצה כדי לחשב את הגיבוב (hash) של integrity, כי זה מסתמך על הבייטים שמוחזרים מהשרת המרוחק ומבטל את ה-SRI.
  • שימוש ב-sha384- כברירת מחדל: המטא-נתונים של התקינות יהיו בפורמט <algorithm>-<base64-digest> באמצעות sha384- (ברירת המחדל המומלצת), sha512- או sha256-.
  • הצמדת משאבי משנה חיצוניים לכתובות URL קבועות עם גרסאות: תמיד צריך להפנות לנכסים של צד שלישי באמצעות כתובות URL קבועות עם גרסאות מוצמדות או כתובות URL שמבוססות על תוכן (כמו https://cdn.example.com/ui@2.4.0/app.min.js), ולא באמצעות כתובות URL משתנות (latest/app.min.js). כתובות URL משתנות חושפות את האתר לשינויים שקטים בשרשרת האספקה ומשבשות את ה-SRI ברגע שהקובץ המרוחק מתעדכן. אם אתם חייבים לטעון סקריפט של צד שלישי שניתן לשינוי ומתעדכן במקום (כמו תג ניתוח נתונים של ספק), אל תכללו את integrity בכתובת ה-URL הזו והגבילו אותה באמצעות Content Security Policy (strict-dynamic / מספרים חד-פעמיים) או ארגז חול (sandbox) של iframe במקום זאת.
  • הפרדה באמצעות רווחים בין כמה תקצירים כשנקודת קצה (endpoint) עשויה להציג כמה עומסי נתונים תקינים: כל הדפדפנים שתומכים ב-SRI מטמיעים את sha256-, sha384- ו-sha512-. אם כתובת URL עשויה להחזיר באופן לגיטימי אחד מכמה מטען ייעודי (payload) תקין וידוע (למשל במהלך בדיקת A/B, השקה מדורגת של CDN או גרסת build של משא ומתן על תוכן), צריך לציין כמה תקצירים של אותו אלגוריתם, מופרדים ברווחים (למשל, 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" (ברירת המחדל המומלצת לנכסי CDN ציבוריים) או crossorigin="use-credentials": חובה בכל פעם ש-integrity מוגדר במשאב ממקורות שונים. השמטה של crossorigin גורמת לדפדפן לאחזר את המשאב במצב no-cors (מה שיוצר תגובה אטומה שלא ניתן לאמת באמצעות גיבוב) ולחסום את המשאב.
  • ‫referrerpolicy="no-referrer" (ברירת המחדל המומלצת לרשתות CDN של צד שלישי) או referrerpolicy="strict-origin-when-cross-origin": הערך הזה מבטל את מדיניות ההפניה של המסמך כדי למנוע דליפה של פרטי מקור או נתיב ב-request header של 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 (חוסר התאמה של התקציר דוחה את ה-promise עם 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 שאוחזר מול התקציר הקריפטוגרפי הצפוי לפני העיבוד או ההחלה שלו.

כשיוצרים משתני url(), צריך לפעול לפי כללי התחביר והאבטחה הבאים של CSS:

  • הוספת מירכאות למחרוזת של כתובת ה-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")
  );
}

אסטרטגיות חלופיות

אם יעד ה-Baseline שלכם עדיין לא תומך בשינוי בקשות 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"> ברמת המסמך משלב 2: בדפדפנים שלא תומכים ב-referrer-policy() בתוך url() של CSS, הכותרת Referrer-Policy: strict-origin-when-cross-origin או הרכיב <meta name="referrer" content="strict-origin-when-cross-origin"> ברמת המסמך מסירים באופן אוטומטי פרמטרים רגישים של נתיבים ופרמטרים של שאילתה בבקשות CSS ממקורות שונים.
  3. הצהרות CSS נפרדות ב-Cascade לשיפור הדרגתי (progressive enhancement): דפדפנים שלא מזהים משתנה בתוך url("..." ...) מתעלמים מכל ההצהרה בזמן הניתוח של ה-CSS. קודם צריך להצהיר על url("...") פשוט, ואז על cross-origin() ו-referrer-policy() (לדפדפנים שתומכים בשני ה-modifiers האלה אבל לא ב-integrity()), ואז על ההצהרה המלאה שכוללת את integrity("sha384-..."):

    • אף פעם אל תשימו url() לא מאומת באותה רשימה של @font-face src שמופרדת בפסיקים: אם כותבים src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); במתאר src יחיד, זה עוקף את Subresource Integrity בזמן הריצה. אם רשת 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. זיהוי תכונות של תמיכה בשינוי url() ב-CSS ב-JavaScript באמצעות 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 };
}