Membatasi koneksi jaringan keluar dengan Daftar Koneksi yang Diizinkan

Header respons HTTP Connection-Allowlist membatasi dokumen atau worker agar hanya berkomunikasi dengan daftar endpoint jaringan yang eksplisit. Tidak seperti Kebijakan Keamanan Konten (CSP)—yang memerlukan pemeliharaan perintah per resource yang terpisah (connect-src, img-src, script-src, font-src, form-action) dan tidak mengatur saluran jaringan spekulatif atau non-Fetch seperti <link rel="dns-prefetch">, <link rel="preconnect">, atau WebRTC—Connection-Allowlist menerapkan daftar yang diizinkan lapisan jaringan tunggal di semua koneksi keluar yang dimulai oleh konteks (fetch(), XMLHttpRequest, subresource, navigator.sendBeacon(), <a ping>, navigasi tingkat atas dan frame, petunjuk resource, Aturan Spekulasi, WebSocket, WebTransport, dan WebRTC).

Cara menerapkan

  1. Serialisasikan daftar yang diizinkan sebagai Daftar Dalam RFC 9651 (...): Format Connection-Allowlist (atau Connection-Allowlist-Report-Only) sebagai daftar item yang dipisahkan spasi dalam satu tanda kurung. Sertakan token tanpa tanda petik response-origin untuk mengizinkan origin dokumen atau worker itu sendiri, diikuti dengan string URLPattern dalam tanda petik ganda untuk setiap endpoint eksternal yang sah yang terhubung atau dituju oleh konteks (API, CDN gambar/font, dan target pengalihan <a href>, <form action>, OAuth, atau pembayaran keluar). Saat membuat pola secara dinamis dari objek konfigurasi atau URL, normalkan ws:/wss: menjadi http:/https: dan hapus garis miring di akhir URL khusus origin (karena new URL('https://api.example.com').href menambahkan /, yang membatasi pencocokan URLPattern hanya ke jalur root /).
  2. Cakupan middleware menurut Sec-Fetch-Dest dan mencakup worker: Browser hanya mematuhi Connection-Allowlist pada respons dokumen/navigasi dan respons skrip worker (nilai Sec-Fetch-Dest document, iframe, worker, sharedworker, dan serviceworker). Saat memvariasikan header menurut Sec-Fetch-Dest, sertakan Vary: Sec-Fetch-Dest agar cache bersama tidak menyajikan respons tanpa header. Menyajikan daftar yang diizinkan yang cocok di skrip Dedicated dan Shared Worker sehingga skrip halaman yang disusupi tidak dapat mengeksfiltrasi data melalui worker.postMessage(). Karena Service Worker melakukan pengambilan jaringan sendiri pada cache tidak ditemukan dan pramuat navigasi, konfigurasikan Connection-Allowlist-nya sebagai superset dari daftar yang diizinkan untuk semua dokumen yang dikontrolnya.
  3. Konfigurasikan pelaporan pelanggaran dan peluncuran bertahap atau ganda: Tentukan header Reporting-Endpoints dan tambahkan parameter ; report-to=<endpoint-name> tanpa tanda petik setelah penutup ). Anda dapat mengirim Connection-Allowlist-Report-Only saja selama peluncuran awal, atau mengirim Connection-Allowlist (menerapkan daftar yang diizinkan dasar) dan Connection-Allowlist-Report-Only (mengaudit daftar yang diizinkan kandidat yang lebih ketat) pada respons yang sama. Laporan pelanggaran dikirim dengan "type": "connection-allowlist" dan isi { url, connection, allowlist, disposition } (dengan connection adalah URL tujuan yang diblokir atau "webrtc", dan disposition adalah "enforce" atau "report").
  4. Perhitungkan pengalihan default dan pemblokiran WebRTC, serta tangani error klien: Secara default, Connection-Allowlist memblokir semua pengalihan HTTP 3xx (redirects=block, bahkan saat mengalihkan antara URL yang diizinkan atau URL origin yang sama) dan semua traffic RTCPeerConnection (webrtc=block). Tambahkan ; redirects=allow hanya jika endpoint tepercaya memerlukan pengalihan sisi server (permintaan awal harus tetap cocok dengan daftar yang diizinkan), atau ; webrtc=allow jika WebRTC diperlukan. Di klien, gabungkan panggilan fetch() keluar di try...catch dan tangani peristiwa WebSocket error sehingga permintaan yang ditolak dengan TypeError (net::ERR_NETWORK_ACCESS_REVOKED) menampilkan status error yang ditangani dan memperbarui status UI, bukan menyebabkan penolakan yang tidak tertangani.

Kode contoh

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 };
  }
}

Praktik terbaik

  • LAKUKAN pemformatan Connection-Allowlist sebagai satu Inner List RFC 9651 (...) yang berisi item yang dipisahkan spasi. Jika header Connection-Allowlist yang tidak kosong gagal mengurai Structured Field tingkat teratas (misalnya, menghilangkan (...) atau menggunakan tanda petik tunggal '...'), browser akan gagal ditutup dengan daftar yang diizinkan yang kosong dan memblokir semua koneksi jaringan keluar. Gunakan Connection-Allowlist: () jika Anda sengaja ingin mencabut semua akses jaringan setelah pemuatan (skema lokal data:, blob:, about:, dan filesystem: tetap diizinkan).
  • LAKUKAN penghapusan garis miring di akhir pada pola URL seluruh origin ("https://api.example.com") atau gunakan karakter pengganti eksplisit ("https://api.example.com/*") sehingga subjalur seperti /v1/users diizinkan. Hapus garis miring di akhir yang ditambahkan oleh new URL(origin).href sebelum membuat serialisasi pola.
  • LAKUKAN pemberian izin endpoint WebSocket (ws:// dan wss://) menggunakan pola "http://..." dan "https://..." di Connection-Allowlist, karena browser menormalisasi skema WebSocket ke HTTP/HTTPS sebelum pencocokan.
  • LAKUKAN penyertaan target navigasi eksternal yang diharapkan (<a href>, <form action>, penyedia OAuth/SSO, checkout pembayaran) dalam daftar yang diizinkan untuk dokumen yang dibatasi, dan ingat bahwa redirects=block default memblokir pengalihan 3xx bahkan yang berasal dari origin yang sama, kecuali jika ; redirects=allow ditentukan.
  • JANGAN mengapit response-origin atau parameter value daftar dalam (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) dalam tanda kutip, atau memisahkan item di dalam (...) dengan koma.
  • JANGAN menggunakan grup ekspresi reguler kustom seperti (\d+) atau jalur relatif seperti "/api/*" di dalam pola URL; parser layanan jaringan browser akan menolak grup dan pola ekspresi reguler tanpa skema dan nama host.
  • JANGAN mengandalkan atribut <iframe connectionAllowlist="..."> eksperimental atau header penerapan tersemat Allow-Connection-Allowlist-From dalam produksi, karena penerapan tersemat tidak diaktifkan secara default di browser pengiriman.

Strategi penggantian

Karena Connection-Allowlist diterapkan di lapisan jaringan browser melalui header respons HTTP, fitur ini tidak dapat di-polyfill di JavaScript sisi klien. Jika target Dasar Pengukuran Anda mencakup browser yang belum mendukung Connection-Allowlist, pasangkan Connection-Allowlist dengan header Content-Security-Policy (CSP) (dan elemen <meta http-equiv="Content-Security-Policy"> untuk pembatasan pengambilan dan subresource tingkat dokumen saat header respons HTTP tidak dapat dikonfigurasi):

  • Defense-in-depth tambahan: Browser pendukung menerapkan kedua kebijakan secara bersamaan—Connection-Allowlist memblokir saluran pemindahan data non-CSP (dns-prefetch, preconnect, WebRTC, dan pengalihan), sedangkan CSP membatasi fetch(), subresource, dan navigasi <form action> di browser tanpa Connection-Allowlist.
  • Sertakan form-action dan worker-src secara eksplisit: Di CSP, default-src tidak di-fallback untuk pengiriman <form action>; selalu sertakan form-action secara eksplisit. Sertakan worker-src 'self' (tidak termasuk blob:) sehingga halaman yang dibatasi tidak dapat menghasilkan SharedWorker blob: untuk mengabaikan batasan tingkat dokumen.
  • Memetakan origin yang diizinkan ke perintah CSP per resource: Tidak seperti Connection-Allowlist (yang menggunakan satu daftar endpoint dan tidak mengatur markup inline), default-src 'self' di CSP membatasi setiap jenis resource (connect-src, img-src, style-src, font-src, script-src) dan memblokir blok <style> dan <script> inline kecuali jika diizinkan melalui hash atau nonce.
  • Sertakan wss:// di connect-src CSP: Meskipun Connection-Allowlist mencocokkan koneksi wss:// dengan pola "https://...", connect-src CSP di lingkungan lintas browser harus secara eksplisit mencantumkan origin wss:// saat WebSocket digunakan.
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'"
/>