כותרת תגובת HTTP של Connection-Allowlist מגבילה מסמך או worker לתקשורת רק עם רשימה מפורשת של נקודות קצה ברשת. בניגוד למדיניות אבטחת תוכן (CSP) – שבה צריך לנהל הוראות נפרדות לכל משאב (connect-src, img-src, script-src, font-src, form-action) והיא לא חלה על ערוצי רשת ספקולטיביים או ערוצים שלא קשורים ל-Fetch, כמו <link rel="dns-prefetch">, <link rel="preconnect"> או WebRTC – Connection-Allowlist אוכפת רשימת היתרים אחת בשכבת הרשת בכל החיבורים היוצאים שנוצרו על ידי ההקשר (fetch(), XMLHttpRequest, משאבי משנה, navigator.sendBeacon(), <a ping>, ניווטים ברמה העליונה ובמסגרת, רמזים למשאבים, כללי ספקולציה, WebSocket, WebTransport ו-WebRTC).
איך מטמיעים
- הופכים את רשימת ההיתרים לרשימה פנימית לפי RFC 9651
(...): הפורמט שלConnection-Allowlist(אוConnection-Allowlist-Report-Only) צריך להיות רשימה אחת בסוגריים של פריטים שמופרדים ברווחים. צריך לכלול את האסימוןresponse-originללא מירכאות כדי לאפשר את המקור של המסמך או ה-worker, ואחריו מחרוזותURLPatternבמירכאות כפולות לכל נקודת קצה (endpoint) חיצונית לגיטימית שההקשר מתחבר או מנווט אליה (ממשקי API, רשתות CDN של תמונות/גופנים ויעדי הפניה לכתובת אחרת יוצאים של<a href>,<form action>, OAuth או תשלום). כשיוצרים תבניות באופן דינמי מאובייקטים של הגדרה או שלURL, צריך לנרמל אתws:/wss:ל-http:/https:ולהסיר לוכסנים בסוף כתובות URL שמכילות רק מקור (כיnew URL('https://api.example.com').hrefמוסיף/, מה שמגביל את ההתאמה שלURLPatternלנתיב הבסיס/בלבד). - הגדרת היקף של תוכנת ביניים לפי
Sec-Fetch-Destוכיסוי של קובצי Worker: דפדפנים מכבדים אתConnection-Allowlistרק בתגובות של מסמכים/ניווט ובתגובות של סקריפטים של קובצי Worker (ערכיSec-Fetch-Destdocument,iframe,worker,sharedworkerו-serviceworker). כשמשנים כותרות לפיSec-Fetch-Dest, צריך לכלול אתVary: Sec-Fetch-Destכדי שמטמונים משותפים לא יציגו תגובות ללא כותרות. צריך להציג רשימת היתרים תואמת בסקריפטים של Dedicated Worker ו-Shared Worker, כדי שסקריפטים של דפים שנפרצו לא יוכלו לחלץ נתונים דרךworker.postMessage(). מכיוון ש-Service Worker מבצע אחזורים משלו ברשת במקרים של אי מציאה במטמון וטעינה מראש של ניווט, צריך להגדיר את ה-Connection-Allowlistשלו כקבוצת-על של רשימות ההיתרים של כל המסמכים שהוא שולט בהם. - הגדרת דיווח על הפרות והשקה מדורגת או כפולה: מגדירים כותרת
Reporting-Endpointsומוסיפים פרמטר; report-to=<endpoint-name>ללא מירכאות אחרי הסוגר). אפשר לשלוח אתConnection-Allowlist-Report-Onlyלבד במהלך ההשקה הראשונית, או לשלוח את שניהםConnection-Allowlist(אכיפה של רשימת היתרים בסיסית) ו-Connection-Allowlist-Report-Only(ביקורת של רשימת היתרים קפדנית יותר) באותה תגובה. דוחות על הפרות נשלחים עם"type": "connection-allowlist"וגוף{ url, connection, allowlist, disposition }(כאשרconnectionהוא כתובת היעד החסומה או"webrtc", ו-dispositionהוא"enforce"או"report"). - הביאו בחשבון את ברירת המחדל של חסימת הפניות אוטומטיות ו-WebRTC, וטפלו בשגיאות בצד הלקוח: כברירת מחדל,
Connection-Allowlistחוסם את כל ההפניות האוטומטיות של3xxHTTP (redirects=block, גם כשמפנים בין כתובות URL ברשימת ההיתרים או כתובות URL מאותו מקור) ואת כל התנועה שלRTCPeerConnection(webrtc=block). הוסיפו את; redirects=allowרק כשנקודות קצה מהימנות דורשות הפניות אוטומטיות בצד השרת (הבקשה הראשונית עדיין צריכה להתאים לרשימת ההיתרים), או את; webrtc=allowכשנדרש WebRTC. בצד הלקוח, צריך לעטוף קריאותfetch()יוצאות ב-try...catchולטפל באירועים שלWebSocketerror, כדי שבקשות שנדחו עםTypeError(net::ERR_NETWORK_ACCESS_REVOKED) יחזירו מצב שגיאה מטופל ויעדכנו את הסטטוס בממשק המשתמש, במקום לגרום לדחיות לא מטופלות.
קוד לדוגמה
Vary: Sec-Fetch-Dest
Reporting-Endpoints: conn-reports="https://app.example.com/api/security-reports"
Connection-Allowlist: (response-origin "https://api.example.com" "https://realtime.example.com" "https://*.cdn.example.com/*"); report-to=conn-reports
Connection-Allowlist-Report-Only: (response-origin "https://api.example.com/v2/*"); report-to=conn-reports
const ALLOWLIST_DESTS = new Set(['document', 'iframe', 'worker', 'sharedworker', 'serviceworker']);
// Normalize a config URL or URLPattern string for Connection-Allowlist:
// - Rewrites ws:// and wss:// to http:// and https:// (WebSocket matching rule).
// - Strips a bare trailing slash on origin-only URLs (e.g. from new URL(...).href)
// so all subpaths on the origin are permitted instead of only "/".
export function normalizeAllowlistPattern(rawPattern) {
const trimmed = String(rawPattern).trim().replace(/^ws(s?):\/\//i, 'http$1://');
if (!/^https?:\/\//i.test(trimmed)) {
throw new Error(`Pattern must include an http:// or https:// scheme: ${rawPattern}`);
}
if (/[\s,]/.test(trimmed)) {
throw new Error(`Pattern must be a single URLPattern without spaces or commas: ${rawPattern}`);
}
if (/[()]/.test(trimmed)) {
throw new Error(`Custom regex groups are rejected by Connection-Allowlist: ${rawPattern}`);
}
return trimmed.replace(/^(https?:\/\/[^/?#]+)\/$/i, '$1');
}
// Build an RFC 9651 Inner List value for Connection-Allowlist or Connection-Allowlist-Report-Only.
export function buildConnectionAllowlistHeader({
allowSelf = true,
patterns = [],
reportTo,
redirects = 'block',
webrtc = 'block',
} = {}) {
const items = allowSelf ? ['response-origin'] : [];
for (const pattern of patterns) {
const raw = String(pattern).trim();
if (raw === 'response-origin') {
if (!items.includes('response-origin')) items.push('response-origin');
continue;
}
const normalized = normalizeAllowlistPattern(raw);
items.push(`"${normalized.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`);
}
let header = `(${items.join(' ')})`;
if (redirects === 'allow') header += '; redirects=allow';
if (webrtc === 'allow') header += '; webrtc=allow';
if (reportTo) {
const endpointToken = String(reportTo).trim().replace(/^"+|"+$/g, '');
header += `; report-to=${endpointToken}`;
}
return header;
}
// Server or middleware helper applying Connection-Allowlist and Reporting-Endpoints
// on document and worker responses.
export function setConnectionAllowlistHeaders(res, options = {}) {
const {
secFetchDest = 'document',
patterns = ['https://api.example.com', 'https://realtime.example.com', 'https://*.cdn.example.com/*'],
reportOnlyPatterns,
reportEndpointName = 'conn-reports',
reportEndpointUrl = 'https://app.example.com/api/security-reports',
redirects = 'block',
webrtc = 'block',
reportOnly = false,
} = options;
const setHeader = (name, value) =>
typeof res.setHeader === 'function' ? res.setHeader(name, value) : res.headers.set(name, value);
if (typeof res.appendHeader === 'function') {
res.appendHeader('Vary', 'Sec-Fetch-Dest');
} else if (typeof res.headers?.append === 'function') {
res.headers.append('Vary', 'Sec-Fetch-Dest');
} else {
setHeader('Vary', 'Sec-Fetch-Dest');
}
if (secFetchDest && !ALLOWLIST_DESTS.has(String(secFetchDest).toLowerCase())) return;
setHeader('Reporting-Endpoints', `${reportEndpointName}="${reportEndpointUrl}"`);
setHeader(
reportOnly ? 'Connection-Allowlist-Report-Only' : 'Connection-Allowlist',
buildConnectionAllowlistHeader({ allowSelf: true, patterns, reportTo: reportEndpointName, redirects, webrtc }),
);
if (!reportOnly && Array.isArray(reportOnlyPatterns)) {
setHeader(
'Connection-Allowlist-Report-Only',
buildConnectionAllowlistHeader({
allowSelf: true,
patterns: reportOnlyPatterns,
reportTo: reportEndpointName,
redirects,
webrtc,
}),
);
}
}
// Client-side helper that catches blocked outbound requests gracefully without unhandled rejections.
export async function fetchWithPolicyHandling(url, { statusElement, ...fetchOptions } = {}) {
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const body = await response.text();
if (statusElement) {
statusElement.textContent = `Allowed: Loaded ${url} (${body.length} bytes).`;
}
return { ok: true, body, error: null };
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (statusElement) {
statusElement.textContent = `Blocked: Request to ${url} was rejected by the outbound connection policy (${message}).`;
}
return { ok: false, body: null, error };
}
}
שיטות מומלצות
- צריך לעצב את
Connection-Allowlistכרשימה פנימית אחת של RFC 9651(...)של פריטים שמופרדים ברווחים. אם כותרתConnection-Allowlistלא ריקה נכשלת בניתוח של שדה מובנה ברמה העליונה (לדוגמה, השמטת(...)או שימוש במירכאות בודדות'...'), הדפדפן נכשל ונסגר עם רשימת היתרים ריקה וחוסם את כל החיבורים לרשת היוצאת. משתמשים ב-Connection-Allowlist: ()כשרוצים לבטל בכוונה את כל הגישה לרשת אחרי הטעינה (הסכימות המקומיותdata:,blob:,about:ו-filesystem:נשארות מותרות). - מומלץ להשמיט את הלוכסן בסוף תבניות של כתובות URL ברמת המקור (
"https://api.example.com") או להשתמש בתו כללי לחיפוש מפורש ("https://api.example.com/*") כדי לאפשר נתיבי משנה כמו/v1/users. צריך להסיר את הלוכסנים בסוף שנוספו על ידיnew URL(origin).hrefלפני שיוצרים סדרות של תבניות. - כן כדאי להוסיף לרשימת ההיתרים את נקודות הקצה של
WebSocket(ws://ו-wss://) באמצעות התבניות"http://..."ו-"https://..."ב-Connection-Allowlist, כי הדפדפן מנרמל את הסכימות של WebSocket ל-HTTP/HTTPS לפני ההתאמה. - מומלץ לכלול יעדי ניווט חיצוניים צפויים (
<a href>,<form action>, ספקי OAuth/SSO, קופות תשלום) ברשימת ההיתרים של מסמך מוגבל, ולזכור שברירת המחדל שלredirects=blockחוסמת אפילו הפניות אוטומטיות של3xxמאותו מקור, אלא אם צוין; redirects=allow. - אל תקיפו במירכאות את
response-originאו את ערכי הפרמטרים של הרשימה הפנימית (; report-to=conn-reports,; redirects=allow,; webrtc=allow), ואל תפרידו פריטים בתוך(...)באמצעות פסיקים. - אל תשתמשו בקבוצות של ביטויים רגולריים בהתאמה אישית כמו
(\d+)או בנתיבים יחסיים כמו"/api/*"בתוך תבניות URL. המנתח של שירות הרשת בדפדפן דוחה קבוצות ותבניות של ביטויים רגולריים בלי סכימה ושם מארח. - אל תסתמכו עדיין על המאפיין הניסיוני
<iframe connectionAllowlist="...">או על כותרת האכיפה המוטמעתAllow-Connection-Allowlist-Fromבסביבת הייצור, כי אכיפה מוטמעת לא מופעלת כברירת מחדל בדפדפנים שמופצים.
אסטרטגיית גיבוי
מכיוון ש-Connection-Allowlist נאכף בשכבת הרשת של הדפדפן באמצעות כותרת תגובת HTTP, אי אפשר להוסיף לו polyfill ב-JavaScript בצד הלקוח. אם יעד ה-Baseline שלכם כולל דפדפנים שעדיין לא תומכים ב-Connection-Allowlist, צריך לשלב את Connection-Allowlist עם כותרת Content-Security-Policy (CSP) (ואלמנט <meta http-equiv="Content-Security-Policy"> לאחזור ברמת המסמך ולהגבלות על משאבי משנה כשאי אפשר להגדיר כותרות של תגובות HTTP):
- הגנה נוספת לעומק: דפדפנים תומכים אוכפים את שתי כללי המדיניות בו-זמנית –
Connection-Allowlistחוסם ערוצי זליגת מידע שאינם CSP (dns-prefetch,preconnect, WebRTC והפניות אוטומטיות), בעוד ש-CSP מגביל אתfetch(), משאבי משנה וניווטים של<form action>בדפדפנים ללאConnection-Allowlist. - צריך לציין במפורש את
form-actionוגם אתworker-src: ב-CSP, אם לא מציינים אתdefault-src, לא נעשה שימוש ב-<form action>כברירת מחדל. תמיד צריך לציין במפורש אתform-action. צריך לכלול אתworker-src 'self'(לא כולל אתblob:) כדי שדפים מוגבלים לא יוכלו ליצורblob:SharedWorkers ולעקוף הגבלות ברמת המסמך. - מיפוי מקורות מורשים להוראות CSP לכל משאב: בניגוד ל-
Connection-Allowlist(שמשתמש ברשימת נקודות קצה אחת ולא מנהל תגי עיצוב מוטבעים),default-src 'self'ב-CSP מגביל כל סוג משאב (connect-src,img-src,style-src,font-src,script-src) וחוסם בלוקים מוטבעים של<style>ו-<script>, אלא אם הם מורשים באמצעות גיבובים או מספרים חד-פעמיים. - Include in CSP (צריך לכלול את
wss://ב-CSPconnect-src):Connection-Allowlistמתאים בין חיבוריwss://לתבניות"https://...", אבל ב-CSPconnect-srcבסביבות של דפדפנים שונים צריך לציין במפורש את המקורות שלwss://כשמשתמשים ב-WebSockets.
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com https://realtime.example.com wss://realtime.example.com; img-src 'self' https://*.cdn.example.com data: blob:; form-action 'self'; worker-src 'self'; object-src 'none'; base-uri 'self'; report-to conn-reports
<!-- Document-level fallback when HTTP response headers cannot be set on static hosting -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src 'self' https://api.example.com https://realtime.example.com wss://realtime.example.com; img-src 'self' https://*.cdn.example.com data: blob:; form-action 'self'; worker-src 'self'; object-src 'none'; base-uri 'self'"
/>