Ausgehende Netzwerkverbindungen mit Verbindungs-Zulassungslisten einschränken

Der HTTP-Antwortheader Connection-Allowlist beschränkt die Kommunikation eines Dokuments oder Workers auf eine explizite Liste von Netzwerkendpunkten. Im Gegensatz zur Content Security Policy (CSP), bei der separate ressourcenspezifische Anweisungen (connect-src, img-src, script-src, font-src, form-action) verwaltet werden müssen und die keine spekulativen oder nicht auf Fetch basierenden Netzwerkkanäle wie <link rel="dns-prefetch">, <link rel="preconnect"> oder WebRTC regelt, erzwingt Connection-Allowlist eine einzige Zulassungsliste auf Netzwerkebene für alle ausgehenden Verbindungen, die vom Kontext initiiert werden (fetch(), XMLHttpRequest, Unterressourcen, navigator.sendBeacon(), <a ping>, Navigationen auf oberster Ebene und Frame-Navigationen, Ressourcenhinweise, Spekulationsregeln, WebSocket, WebTransport und WebRTC).

Implementierung

  1. Zulassungsliste als RFC 9651-Inner List (...) serialisieren: Formatieren Sie Connection-Allowlist (oder Connection-Allowlist-Report-Only) als einzelne Liste mit durch Leerzeichen getrennten Elementen in Klammern. Fügen Sie das Token response-origin ohne Anführungszeichen ein, um den Ursprung des Dokuments oder Workers zuzulassen, gefolgt von Strings in doppelten Anführungszeichen (URLPattern) für jeden legitimen externen Endpunkt, zu dem der Kontext eine Verbindung herstellt oder zu dem er weiterleitet (APIs, Bild-/Schriftart-CDNs und ausgehende <a href>-, <form action>-, OAuth- oder Zahlungsweiterleitungsziele). Wenn Sie Muster dynamisch aus Konfigurations- oder URL-Objekten erstellen, normalisieren Sie ws:/wss: zu http:/https: und entfernen Sie nachgestellte Schrägstriche bei URLs, die nur den Ursprung enthalten (da new URL('https://api.example.com').href / anhängt, wodurch der URLPattern-Abgleich nur auf den Stammpfad / beschränkt wird).
  2. Middleware nach Sec-Fetch-Dest eingrenzen und Worker abdecken: Browser berücksichtigen Connection-Allowlist nur bei Dokument-/Navigationsantworten und Worker-Skriptantworten (Sec-Fetch-Dest-Werte document, iframe, worker, sharedworker und serviceworker). Wenn Sie Header nach Sec-Fetch-Dest variieren, fügen Sie Vary: Sec-Fetch-Dest hinzu, damit freigegebene Caches keine Antworten ohne Header bereitstellen. Stellen Sie eine passende Zulassungsliste für dedizierte und freigegebene Worker-Skripts bereit, damit kompromittierte Seitenskripts keine Daten über worker.postMessage() exfiltrieren können. Da ein Service Worker bei Cache-Fehlern und Navigations-Preloads eigene Netzwerkabrufe durchführt, sollten Sie seine Connection-Allowlist als Superset der Zulassungslisten aller von ihm gesteuerten Dokumente konfigurieren.
  3. Verstoßberichte und gestaffelten oder dualen Roll-out konfigurieren: Definieren Sie einen Reporting-Endpoints-Header und hängen Sie nach dem schließenden ) einen ; report-to=<endpoint-name>-Parameter ohne Anführungszeichen an. Sie können während des ersten Rollouts nur Connection-Allowlist-Report-Only senden oder beide Connection-Allowlist (Erzwingen einer Baseline-Zulassungsliste) und Connection-Allowlist-Report-Only (Prüfen einer strengeren Kandidaten-Zulassungsliste) in derselben Antwort senden. Verstoßberichte werden mit "type": "connection-allowlist" und Textkörper { url, connection, allowlist, disposition } gesendet (wobei connection die blockierte Ziel-URL oder "webrtc" ist und disposition entweder "enforce" oder "report" ist).
  4. Standardweiterleitung und WebRTC-Blockierung berücksichtigen und Clientfehler beheben: Standardmäßig blockiert Connection-Allowlist alle 3xx-HTTP-Weiterleitungen (redirects=block, auch bei Weiterleitungen zwischen URLs auf der Zulassungsliste oder URLs mit demselben Ursprung) und den gesamten RTCPeerConnection-Traffic (webrtc=block). Hängen Sie ; redirects=allow nur an, wenn vertrauenswürdige Endpunkte serverseitige Weiterleitungen erfordern (die ursprüngliche Anfrage muss weiterhin mit der Zulassungsliste übereinstimmen), oder ; webrtc=allow, wenn WebRTC erforderlich ist. Verpacken Sie auf dem Client ausgehende fetch()-Aufrufe in try...catch und verarbeiten Sie WebSocket-error-Ereignisse, damit Anfragen, die mit TypeError (net::ERR_NETWORK_ACCESS_REVOKED) abgelehnt werden, einen verarbeiteten Fehlerstatus zurückgeben und den UI-Status aktualisieren, anstatt nicht verarbeitete Ablehnungen zu verursachen.

Beispielcode

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 Practices

  • DO formatieren Sie Connection-Allowlist als einzelne RFC 9651-interne Liste (...) mit durch Leerzeichen getrennten Elementen. Wenn ein nicht leerer Connection-Allowlist-Header die Parsing-Prüfung für strukturierte Felder auf oberster Ebene nicht besteht (z. B. wenn (...) weggelassen wird oder einfache Anführungszeichen '...' verwendet werden), schlägt der Browser mit einer leeren Zulassungsliste fehl und blockiert alle ausgehenden Netzwerkverbindungen. Verwenden Sie Connection-Allowlist: (), wenn Sie den gesamten Netzwerkzugriff nach dem Laden absichtlich widerrufen möchten. Lokale Schemas (data:, blob:, about: und filesystem:) bleiben zulässig.
  • DO omit the trailing slash on origin-wide URL patterns ("https://api.example.com") or use an explicit wildcard ("https://api.example.com/*") so subpaths like /v1/users are permitted. Entfernen Sie abschließende Schrägstriche, die von new URL(origin).href hinzugefügt wurden, bevor Sie Muster serialisieren.
  • LASSEN Sie WebSocket-Endpunkte (ws:// und wss://) mit den Mustern "http://..." und "https://..." in Connection-Allowlist zu, da der Browser WebSocket-Schemas vor dem Abgleich in HTTP/HTTPS normalisiert.
  • Fügen Sie erwartete externe Navigationsziele (<a href>, <form action>, OAuth-/SSO-Anbieter, Zahlungsabwicklung) der Zulassungsliste eines eingeschränkten Dokuments hinzu. Beachten Sie, dass die Standardeinstellung redirects=block sogar 3xx-Weiterleitungen mit derselben Quelle blockiert, sofern nicht ; redirects=allow angegeben ist.
  • Setzen Sie response-origin- oder Parameterwerte für innere Listen (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) NICHT in Anführungszeichen und trennen Sie Elemente innerhalb von (...) nicht durch Kommas.
  • Verwenden Sie KEINE benutzerdefinierten regulären Ausdrucksgruppen wie (\d+) oder relative Pfade wie "/api/*" in URL-Mustern. Der Netzwerkdienst-Parser des Browsers lehnt Regex-Gruppen und Muster ohne Schema und Hostnamen ab.
  • Verlassen Sie sich in der Produktion NICHT auf das experimentelle Attribut <iframe connectionAllowlist="..."> oder den eingebetteten Header Allow-Connection-Allowlist-From für die Erzwingung, da die eingebettete Erzwingung in Browsern, die ausgeliefert werden, nicht standardmäßig aktiviert ist.

Fallback-Strategie

Da Connection-Allowlist auf der Browser-Netzwerkschicht über einen HTTP-Antwortheader erzwungen wird, kann es nicht in clientseitigem JavaScript polyfilled werden. Wenn Ihr Baseline-Ziel Browser umfasst, die Connection-Allowlist noch nicht unterstützen, kombinieren Sie Connection-Allowlist mit einem Content-Security-Policy-Header (CSP) (und einem <meta http-equiv="Content-Security-Policy">-Element für Abruf- und Unterressourcenbeschränkungen auf Dokumentebene, wenn HTTP-Antwortheader nicht konfiguriert werden können):

  • Additive Defense-in-Depth: Unterstützende Browser erzwingen beide Richtlinien gleichzeitig. Connection-Allowlist blockiert Exfiltrationskanäle, die nicht durch CSP abgedeckt sind (dns-prefetch, preconnect, WebRTC und Weiterleitungen), während CSP fetch(), Unterressourcen und <form action>-Navigationen in Browsern ohne Connection-Allowlist einschränkt.
  • form-action und worker-src explizit abdecken: In CSP wird default-src nicht für <form action>-Einreichungen verwendet. Schließen Sie form-action immer explizit ein. Fügen Sie worker-src 'self' (ohne blob:) ein, damit eingeschränkte Seiten keine blob:-SharedWorker erstellen können, um Einschränkungen auf Dokumentebene zu umgehen.
  • Zulässige Ursprünge den CSP-Anweisungen pro Ressource zuordnen: Im Gegensatz zu Connection-Allowlist (wo eine einzelne Endpunktliste verwendet wird und Inline-Markup nicht geregelt wird) schränkt default-src 'self' in CSP jeden Ressourcentyp (connect-src, img-src, style-src, font-src, script-src) ein und blockiert Inline-<style>- und <script>-Blöcke, sofern sie nicht über Hashes oder Nonces autorisiert sind.
  • wss:// in CSP connect-src aufnehmen: Während Connection-Allowlist wss://-Verbindungen mit "https://..."-Mustern abgleicht, sollten in CSP connect-src in browserübergreifenden Umgebungen explizit wss://-Ursprünge aufgeführt werden, wenn WebSockets verwendet werden.
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'"
/>