כשאתם טוענים סקריפטים, גיליונות סגנונות, גופנים או תמונות מרשתות 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-facesrcו-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 מדורגות כדי שמשאבי המשנה יישארו פונקציונליים ומוגנים:
- כשיש צורך ב-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בכל פעם שצריך לאכוף אימות תקינות בכל דפדפן. - להסתמך על הכותרת
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 ממקורות שונים. הצהרות CSS נפרדות ב-Cascade לשיפור הדרגתי (progressive enhancement): דפדפנים שלא מזהים משתנה בתוך
url("..." ...)מתעלמים מכל ההצהרה בזמן הניתוח של ה-CSS. קודם צריך להצהיר עלurl("...")פשוט, ואז עלcross-origin()ו-referrer-policy()(לדפדפנים שתומכים בשני ה-modifiers האלה אבל לא ב-integrity()), ואז על ההצהרה המלאה שכוללת אתintegrity("sha384-..."):- אף פעם אל תשימו
url()לא מאומת באותה רשימה של@font-facesrcשמופרדת בפסיקים: אם כותבים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") ); }- אף פעם אל תשימו
זיהוי תכונות של תמיכה בשינוי
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 };
}