Limita le connessioni di rete in uscita con le liste consentite di connessioni

Funzionalità web: Liste consentite di connessioni

L'intestazione della risposta HTTP Connection-Allowlist limita un documento o un worker alla comunicazione solo con un elenco esplicito di endpoint di rete. A differenza della Content Security Policy (CSP), che richiede la gestione di istruzioni separate per risorsa (connect-src, img-src, script-src, font-src, form-action) e non regola i canali di rete speculativi o non di recupero come <link rel="dns-prefetch">, <link rel="preconnect"> o WebRTC, Connection-Allowlist applica una singola lista consentita a livello di rete per tutte le connessioni in uscita avviate dal contesto (fetch(), XMLHttpRequest, sottorisorse, navigator.sendBeacon(), <a ping>, navigazioni di primo livello e dei frame, suggerimenti sulle risorse, regole di speculazione, WebSocket, WebTransport e WebRTC).

Come implementare

  1. Serializza la lista consentita come elenco interno RFC 9651 (...): formatta Connection-Allowlist (o Connection-Allowlist-Report-Only) come un singolo elenco tra parentesi di elementi separati da spazi. Includi il token senza virgolette response-origin per consentire l'origine del documento o del worker, seguito da stringhe URLPattern tra virgolette doppie per ogni endpoint esterno legittimo a cui il contesto si connette o naviga (API, CDN di immagini/caratteri e target di reindirizzamento in uscita <a href>, <form action>, OAuth o di pagamento). Quando crei pattern in modo dinamico dalla configurazione o dagli oggetti URL, normalizza ws:/wss: in http:/https: e rimuovi gli slash finali vuoti negli URL di sola origine (perché new URL('https://api.example.com').href aggiunge /, il che limita la corrispondenza di URLPattern solo al percorso root /).
  2. Definisci l'ambito del middleware in base a Sec-Fetch-Dest e copri i worker: i browser rispettano Connection-Allowlist solo nelle risposte di documenti/navigazione e nelle risposte degli script dei worker (valori Sec-Fetch-Dest document, iframe, worker, sharedworker e serviceworker). Quando modifichi le intestazioni in base a Sec-Fetch-Dest, includi Vary: Sec-Fetch-Dest in modo che le cache condivise non forniscano risposte senza intestazioni. Pubblica una lista consentita corrispondente sugli script Dedicated e Shared Worker in modo che gli script di pagina compromessi non possano esfiltrare i dati tramite worker.postMessage(). Poiché un service worker esegue i propri recuperi di rete in caso di fallimenti della cache e precaricamenti di navigazione, configura il relativo Connection-Allowlist come un superset delle liste consentite di tutti i documenti che controlla.
  3. Configura i report sulle violazioni e l'implementazione temporanea o doppia: definisci un'intestazione Reporting-Endpoints e aggiungi un parametro ; report-to=<endpoint-name> senza virgolette dopo la chiusura di ). Puoi inviare Connection-Allowlist-Report-Only da solo durante l'implementazione iniziale o inviare sia Connection-Allowlist (per applicare una lista consentita di base) sia Connection-Allowlist-Report-Only (per controllare una lista consentita di candidati più rigorosa) nella stessa risposta. I report sulle violazioni vengono inviati con "type": "connection-allowlist" e corpo { url, connection, allowlist, disposition } (dove connection è l'URL di destinazione bloccato o "webrtc" e disposition è "enforce" o "report").
  4. Considera il reindirizzamento predefinito e il blocco di WebRTC e gestisci gli errori del client: per impostazione predefinita, Connection-Allowlist blocca tutti i reindirizzamenti HTTP 3xx (redirects=block, anche quando il reindirizzamento avviene tra URL inclusi nella lista consentita o URL di origine identica) e tutto il traffico RTCPeerConnection (webrtc=block). Aggiungi ; redirects=allow solo quando gli endpoint attendibili richiedono reindirizzamenti lato server (la richiesta iniziale deve comunque corrispondere alla lista consentita) o ; webrtc=allow quando è richiesto WebRTC. Sul client, racchiudi le chiamate fetch() in uscita in try...catch e gestisci gli eventi WebSocket error in modo che le richieste rifiutate con TypeError (net::ERR_NETWORK_ACCESS_REVOKED) restituiscano uno stato di errore gestito e aggiornino lo stato dell'UI anziché causare rifiuti non gestiti.

Esempio di codice

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

Best practice

  • DO format Connection-Allowlist as a single RFC 9651 Inner List (...) of space-separated items. Se un'intestazione Connection-Allowlist non vuota non supera l'analisi del campo strutturato di primo livello (ad esempio, omettendo (...) o utilizzando virgolette singole '...'), il browser non si chiude con una lista consentita vuota e blocca tutte le connessioni di rete in uscita. Utilizza Connection-Allowlist: () quando vuoi revocare intenzionalmente tutti gli accessi alla rete dopo il caricamento (gli schemi locali data:, blob:, about: e filesystem: rimangono consentiti).
  • SÌ: ometti lo slash finale nei pattern URL a livello di origine ("https://api.example.com") o utilizza un carattere jolly esplicito ("https://api.example.com/*") in modo che i percorsi secondari come /v1/users siano consentiti. Rimuovi gli slash finali aggiunti da new URL(origin).href prima di serializzare i pattern.
  • DEVI inserire nella lista consentita gli endpoint WebSocket (ws:// e wss://) utilizzando i pattern "http://..." e "https://..." in Connection-Allowlist, perché il browser normalizza gli schemi WebSocket in HTTP/HTTPS prima della corrispondenza.
  • INCLUDI i target di navigazione esterni previsti (<a href>, <form action>, provider OAuth/SSO, checkout dei pagamenti) nella lista consentita di un documento limitato e ricorda che il valore predefinito redirects=block blocca anche i reindirizzamenti 3xx della stessa origine, a meno che non venga specificato ; redirects=allow.
  • NON racchiudere tra virgolette i valori dei parametri response-origin o dell'elenco interno (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) né separare gli elementi all'interno di (...) con virgole.
  • NON utilizzare gruppi di espressioni regolari personalizzati come (\d+) o percorsi relativi come "/api/*" all'interno dei pattern URL; l'analizzatore del servizio di rete del browser rifiuta i gruppi e i pattern di espressioni regolari senza uno schema e un nome host.
  • NON fare ancora affidamento sull'attributo sperimentale <iframe connectionAllowlist="..."> o sull'intestazione di applicazione incorporata Allow-Connection-Allowlist-From in produzione, poiché l'applicazione incorporata non è abilitata per impostazione predefinita nei browser di spedizione.

Strategia di riserva

Poiché Connection-Allowlist viene applicato a livello di rete del browser tramite un'intestazione della risposta HTTP, non è possibile eseguire il polyfill in JavaScript lato client. Se il tuo target Baseline include browser che non supportano ancora Connection-Allowlist, associa Connection-Allowlist a un'intestazione Content-Security-Policy (CSP) (e a un elemento <meta http-equiv="Content-Security-Policy"> per le limitazioni di recupero e delle risorse secondarie a livello di documento quando non è possibile configurare le intestazioni della risposta HTTP):

  • Difesa additiva in profondità: I browser che supportano queste politiche applicano entrambe simultaneamente—Connection-Allowlist blocca i canali di esfiltrazione non-CSP (dns-prefetch ,preconnect , WebRTC e reindirizzamenti), mentre CSP limitafetch() , sottorisorse e<form action> navigazione nei browser senzaConnection-Allowlist.
  • Copri form-action e worker-src in modo esplicito: in CSP, default-src non funge da riserva per gli invii <form action>; includi sempre form-action in modo esplicito. Includi worker-src 'self' (escluso blob:) in modo che le pagine con limitazioni non possano generare SharedWorker blob: per aggirare le limitazioni a livello di documento.
  • Mappa le origini consentite alle direttive CSP per risorsa: a differenza di Connection-Allowlist (che utilizza un unico elenco di endpoint e non regola il markup incorporato), default-src 'self' in CSP limita ogni tipo di risorsa (connect-src, img-src, style-src, font-src, script-src) e blocca i blocchi <style> e <script> incorporati, a meno che non siano autorizzati tramite hash o nonce.
  • Includi wss:// in CSP connect-src: mentre Connection-Allowlist confronta le connessioni wss:// con i pattern "https://...", CSP connect-src in ambienti cross-browser deve elencare esplicitamente le origini wss:// quando vengono utilizzati i WebSocket.
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'"
/>