يفرض عنوان الاستجابة Connection-Allowlist HTTP قيودًا على المستند أو العامل، بحيث لا يمكنه التواصل إلا مع قائمة صريحة من نقاط نهاية الشبكة. على عكس "سياسة أمان المحتوى" (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بدون علامات اقتباس للسماح بمصدر المستند أو العامل، متبوعًا بسلاسلURLPatternبين علامتَي اقتباس مزدوجتَين لكل نقطة نهاية خارجية شرعية يتصل بها السياق أو ينتقل إليها (واجهات برمجة التطبيقات أو شبكات توصيل المحتوى (CDN) للصور/الخطوط وأهداف إعادة التوجيه الصادرة لـ<a href>أو<form action>أو OAuth أو الدفع). عند إنشاء الأنماط ديناميكيًا من الإعدادات أو عناصرURL، يجب تسويةws:/wss:إلىhttp:/https:وإزالة الشرطات المائلة اللاحقة في عناوين URL التي تحتوي على المصدر فقط (لأنّnew URL('https://api.example.com').hrefيضيف/، ما يقيّد مطابقةURLPatternبمسار الجذر/فقط). - تحديد نطاق البرمجيات الوسيطة حسب
Sec-Fetch-Destوتغطية العاملين: تلتزم المتصفحات فقط بـConnection-Allowlistفي ردود المستندات/التنقّل وردود النصوص البرمجية للعاملين (قيمSec-Fetch-Destdocumentوiframeوworkerوsharedworkerوserviceworker). عند تغيير العناوين حسبSec-Fetch-Dest، يجب تضمينVary: Sec-Fetch-Destحتى لا تعرض ذاكرات التخزين المؤقت المشتركة ردودًا بدون عناوين. يجب عرض قائمة مسموح بها مطابقة على نصوص Dedicated و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هو عنوان URL المقصود المحظور أو"webrtc"، وdispositionهو"enforce"أو"report"). - مراعاة الإعدادات التلقائية لإعادة التوجيه وحظر WebRTC ومعالجة أخطاء العميل: يحظر
Connection-Allowlistتلقائيًا جميع عمليات إعادة التوجيه عبر بروتوكول HTTP في3xx(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/الدخول الموحّد وعمليات الدفع) في القائمة المسموح بها للمستندات المحظورة، مع العلم أنّ السمة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قنوات استخراج البيانات غير المتوافقة مع "سياسة أمان المحتوى" (dns-prefetchوpreconnectوWebRTC وعمليات إعادة التوجيه)، بينما تفرض "سياسة أمان المحتوى" قيودًا علىfetch()والموارد الفرعية وعمليات التنقّل في<form action>في المتصفّحات التي لا تتضمّنConnection-Allowlist. - تغطية
form-actionوworker-srcبشكل صريح: في سياسة أمان المحتوى، لا يتم استخدام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>المضمّنة ما لم يتم السماح بها من خلال تجزئات أو أرقام عشوائية. - تضمين
wss://في سياسة أمان المحتوى (CSP)connect-src: على الرغم من أنّConnection-Allowlistيطابق اتصالاتwss://مع أنماط"https://..."، يجب أن تدرج سياسة أمان المحتوى (CSP)connect-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'"
/>