Uitgaande netwerkverbindingen beperken met toelatingslijsten voor verbindingen

De HTTP-reactieheader Connection-Allowlist beperkt een document of worker tot communicatie met alleen een expliciete lijst van netwerkeindpunten. In tegenstelling tot Content Security Policy (CSP), waarvoor afzonderlijke richtlijnen per bron (connect-src, img-src, script-src, font-src, form-action) moeten worden onderhouden en die geen speculatieve of niet-Fetch-netwerkkanalen zoals <link rel="dns-prefetch">, <link rel="preconnect"> of WebRTC beheert, dwingt Connection-Allowlist één toelatingslijst op netwerklaag af voor alle uitgaande verbindingen die door de context worden gestart (fetch(), XMLHttpRequest, subbronnen, navigator.sendBeacon(), <a ping>, navigatie op het hoogste niveau en in frames, brontips, speculatieregels, WebSocket, WebTransport en WebRTC).

Hoe wordt dit geïmplementeerd

  1. Serialiseer de toelatingslijst als een RFC 9651 Inner List (...): Maak de indeling Connection-Allowlist (of Connection-Allowlist-Report-Only) als één lijst tussen haakjes met door spaties gescheiden items. Neem de niet-geciteerde token response-origin op om de eigen oorsprong van het document of de worker toe te staan, gevolgd door tekenreeksen tussen dubbele aanhalingstekens URLPattern voor elk legitiem extern eindpunt waarmee de context verbinding maakt of waarnaar de context navigeert (API's, CDN's voor afbeeldingen/lettertypen en uitgaande <a href>, <form action>, OAuth of omleidingstargets voor betalingen). Als je patronen dynamisch maakt op basis van configuratie- of URL-objecten, normaliseer je ws:/wss: naar http:/https: en verwijder je de afsluitende slashes in URL's die alleen de oorsprong bevatten (omdat new URL('https://api.example.com').href / toevoegt, waardoor URLPattern-overeenkomsten worden beperkt tot alleen het hoofdpad /).
  2. Middleware bereik op Sec-Fetch-Dest en dek werkers: Browsers respecteren Connection-Allowlist alleen voor document-/navigatiereacties en reacties van werkerscripts (Sec-Fetch-Dest-waarden document, iframe, worker, sharedworker en serviceworker). Als je headers varieert op Sec-Fetch-Dest, neem je Vary: Sec-Fetch-Dest op zodat gedeelde caches geen reacties zonder headers leveren. Lever een overeenkomende toelatingslijst aan voor Dedicated en Shared Worker-scripts, zodat gehackte paginascripts geen gegevens kunnen exfiltreren via worker.postMessage(). Omdat een servicemedewerker eigen netwerkfetches uitvoert bij ontbrekende cache-items en navigatie-preloads, stel je de Connection-Allowlist in als een superset van de toelatingslijsten van alle documenten die de servicemedewerker beheert.
  3. Rapportage van schendingen en gefaseerde of dubbele implementatie instellen: Definieer een Reporting-Endpoints-koptekst en voeg een niet-geciteerde ; report-to=<endpoint-name>-parameter toe na de afsluitende ). Je kunt Connection-Allowlist-Report-Only alleen sturen tijdens de eerste implementatie of beide Connection-Allowlist (een basislijst met toegestane entiteiten afdwingen) en Connection-Allowlist-Report-Only (een strengere kandidatenlijst met toegestane entiteiten controleren) sturen in dezelfde reactie. Schendingsrapporten worden geleverd met "type": "connection-allowlist" en hoofdtekst { url, connection, allowlist, disposition } (waarbij connection de geblokkeerde bestemmings-URL of "webrtc" is en disposition "enforce" of "report" is).
  4. Houd rekening met standaard omleidingen en WebRTC-blokkering en verwerk clientfouten: Standaard blokkeert Connection-Allowlist alle 3xx HTTP-omleidingen (redirects=block, zelfs bij omleiding tussen URL's op de toelatingslijst of URL's met dezelfde oorsprong) en al het RTCPeerConnection-verkeer (webrtc=block). Voeg ; redirects=allow alleen toe als vertrouwde eindpunten omleidingen aan de serverzijde vereisen (het eerste verzoek moet nog steeds overeenkomen met de toelatingslijst) of ; webrtc=allow als WebRTC is vereist. Verpak op de client uitgaande fetch()-aanroepen in try...catch en verwerk WebSocket error-gebeurtenissen, zodat verzoeken die worden afgewezen met TypeError (net::ERR_NETWORK_ACCESS_REVOKED) een verwerkte foutstatus retourneren en de UI-status updaten in plaats van onverwerkte afwijzingen te veroorzaken.

Voorbeeldcode

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

Praktische tips

  • WEL: Maak de indeling van Connection-Allowlist als één RFC 9651 Inner List (...) van door spaties gescheiden items. Als een niet-lege Connection-Allowlist-header de parsering van het gestructureerde veld op het hoogste niveau niet doorstaat (bijvoorbeeld door (...) weg te laten of enkele aanhalingstekens '...' te gebruiken), wordt de browser gesloten met een lege toelatingslijst en worden alle uitgaande netwerkverbindingen geblokkeerd. Gebruik Connection-Allowlist: () als je opzettelijk alle netwerktoegang wilt intrekken na het laden (lokale schema's data:, blob:, about: en filesystem: blijven toegestaan).
  • WEL: Laat de afsluitende slash weg bij URL-patronen voor de hele oorsprong ("https://api.example.com") of gebruik een expliciete wildcard ("https://api.example.com/*") zodat subpaden zoals /v1/users zijn toegestaan. Verwijder afsluitende slashes die zijn toegevoegd door new URL(origin).href voordat patronen worden geserialiseerd.
  • Zet WEL WebSocket-eindpunten (ws:// en wss://) op de toelatingslijst met de patronen "http://..." en "https://..." in Connection-Allowlist, omdat de browser WebSocket-schema's normaliseert naar HTTP/HTTPS voordat deze worden vergeleken.
  • Voeg verwachte externe navigatiedoelen (<a href>, <form action>, OAuth-/SSO-providers, betaalpagina's) toe aan de toelatingslijst van een beperkt document en onthoud dat de standaard redirects=block zelfs 3xx-omleidingen van dezelfde oorsprong blokkeert, tenzij ; redirects=allow is opgegeven.
  • Zet response-origin of parameterwaarden voor interne lijsten (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) NIET tussen aanhalingstekens en scheid items in (...) niet met komma's.
  • Gebruik GEEN aangepaste groepen met reguliere expressies, zoals (\d+), of relatieve paden, zoals "/api/*", in URL-patronen. De parser van de netwerkservice van de browser weigert regex-groepen en -patronen zonder schema en hostnaam.
  • Vertrouw nog NIET op het experimentele kenmerk <iframe connectionAllowlist="..."> of de ingesloten handhavingsheader Allow-Connection-Allowlist-From in de productie, omdat ingesloten handhaving niet standaard aanstaat in verzendbrowsers.

Reservestrategie

Omdat Connection-Allowlist wordt afgedwongen op de netwerklaag van de browser via een HTTP-reactieheader, kan deze niet worden gepolyfilled in JavaScript aan de clientzijde. Als je Baseline-doel browsers bevat die Connection-Allowlist nog niet ondersteunen, koppel je Connection-Allowlist aan een Content-Security-Policy-header (CSP) (en een <meta http-equiv="Content-Security-Policy">-element voor ophalen op documentniveau en beperkingen voor subbronnen als HTTP-reactieheaders niet kunnen worden geconfigureerd):

  • Additieve diepgaande verdediging: Ondersteunende browsers dwingen beide beleidsregels tegelijkertijd af. Connection-Allowlist blokkeert niet-CSP-exfiltratiekanalen (dns-prefetch, preconnect, WebRTC en omleidingen), terwijl CSP fetch(), subbronnen en <form action>-navigaties beperkt in browsers zonder Connection-Allowlist.
  • Dek form-action en worker-src expliciet af: In CSP valt default-src niet terug voor <form action>-inzendingen. Neem form-action altijd expliciet op. Neem worker-src 'self' op (met uitzondering van blob:), zodat beperkte pagina's geen blob: SharedWorkers kunnen genereren om beperkingen op documentniveau te omzeilen.
  • Toegestane herkomsten toewijzen aan CSP-instructies per bron: In tegenstelling tot Connection-Allowlist (die één eindpuntlijst gebruikt en geen inline opmaak beheert), beperkt default-src 'self' in CSP elk brontype (connect-src, img-src, style-src, font-src, script-src) en blokkeert inline <style>- en <script>-blokken, tenzij deze zijn geautoriseerd via hashes of nonces.
  • Neem wss:// op in CSP connect-src: Hoewel Connection-Allowlist wss://-verbindingen vergelijkt met "https://..."-patronen, moet CSP connect-src in omgevingen met meerdere browsers expliciet wss://-oorsprongen vermelden als WebSockets worden gebruikt.
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'"
/>