Ograniczanie połączeń sieciowych wychodzących za pomocą list dozwolonych połączeń

Funkcje internetowe: Listy dozwolonych połączeń

Nagłówek odpowiedzi HTTP Connection-Allowlist ogranicza komunikację dokumentu lub skryptu service worker tylko do określonej listy punktów końcowych sieci. W przeciwieństwie do standardu Content Security Policy (CSP), który wymaga utrzymywania oddzielnych dyrektyw dla poszczególnych zasobów (connect-src, img-src, script-src, font-src, form-action) i nie zarządza spekulacyjnymi ani innymi niż Fetch kanałami sieciowymi, takimi jak <link rel="dns-prefetch">, <link rel="preconnect"> czy WebRTC, Connection-Allowlist wymusza stosowanie jednej listy dozwolonych w warstwie sieciowej we wszystkich połączeniach wychodzących inicjowanych przez kontekst (fetch(), XMLHttpRequest, zasoby podrzędne, navigator.sendBeacon(), <a ping>, nawigacja najwyższego poziomu i nawigacja w ramkach, wskazówki dotyczące zasobów, reguły spekulacji, WebSocket, WebTransport i WebRTC).

Jak to zrobić?

  1. Zserializuj listę dozwolonych jako listę wewnętrzną RFC 9651 (...): sformatuj Connection-Allowlist (lub Connection-Allowlist-Report-Only) jako pojedynczą listę elementów rozdzielonych spacjami w nawiasach. Uwzględnij token response-origin bez cudzysłowu, aby zezwolić na źródło dokumentu lub skryptu, a następnie ciągi znaków URLPattern w cudzysłowie dla każdego prawidłowego zewnętrznego punktu końcowego, z którym łączy się kontekst lub do którego przechodzi (interfejsy API, sieci CDN obrazów/czcionek i wychodzące elementy docelowe <a href>, <form action>, OAuth lub przekierowania płatności). Podczas dynamicznego tworzenia wzorców z konfiguracji lub obiektów URL znormalizuj ws:/wss: do http:/https: i usuń końcowe ukośniki z adresów URL zawierających tylko źródło (ponieważ new URL('https://api.example.com').href dołącza /, co ogranicza dopasowanie URLPattern tylko do ścieżki głównej /).
  2. Zakres oprogramowania pośredniczącego według Sec-Fetch-Dest i pracowników: przeglądarki uwzględniają Connection-Allowlist tylko w odpowiedziach dokumentu/nawigacji i odpowiedziach skryptu pracownika (wartości Sec-Fetch-Dest: document, iframe, worker, sharedworker i serviceworker). Podczas zmieniania nagłówków według Sec-Fetch-Dest uwzględnij Vary: Sec-Fetch-Dest, aby udostępnione pamięci podręczne nie wyświetlały odpowiedzi bez nagłówka. Używaj pasującej listy dozwolonych w skryptach Dedicated i Shared Worker, aby skrypty na przejętych stronach nie mogły wyodrębniać danych przez worker.postMessage(). Skrypt service worker pobiera dane z sieci w przypadku braku trafień w pamięci podręcznej i wstępnego wczytywania nawigacji, dlatego skonfiguruj jego nagłówek Connection-Allowlist jako nadzbiór list dozwolonych wszystkich dokumentów, które kontroluje.
  3. Skonfiguruj raportowanie naruszeń i wdrażanie etapowe lub podwójne: zdefiniuj nagłówek Reporting-Endpoints i dołącz parametr ; report-to=<endpoint-name> bez cudzysłowu po zamykającym ). Podczas początkowego wdrażania możesz wysyłać tylko Connection-Allowlist-Report-Only lub wysyłać zarówno Connection-Allowlist (egzekwowanie podstawowej listy dozwolonych), jak i Connection-Allowlist-Report-Only (kontrola bardziej rygorystycznej listy dozwolonych) w tej samej odpowiedzi. Raporty o naruszeniach są dostarczane z "type": "connection-allowlist" i treścią { url, connection, allowlist, disposition } (gdzie connection to zablokowany docelowy adres URL lub "webrtc", a disposition to "enforce" lub "report").
  4. Uwzględnij domyślne przekierowanie i blokowanie WebRTC oraz obsługuj błędy klienta: domyślnie Connection-Allowlist blokuje wszystkie przekierowania HTTP 3xx (redirects=block, nawet podczas przekierowywania między adresami URL z listy dozwolonych lub adresami URL źródła) i cały ruch RTCPeerConnection (webrtc=block). Dodaj ; redirects=allow tylko wtedy, gdy zaufane punkty końcowe wymagają przekierowań po stronie serwera (początkowe żądanie musi być zgodne z listą dozwolonych) lub ; webrtc=allow, gdy wymagany jest WebRTC. Na kliencie opakuj wychodzące wywołania fetch() w try...catch i obsługuj zdarzenia WebSocket error, aby żądania odrzucone z powodu TypeError (net::ERR_NETWORK_ACCESS_REVOKED) zwracały obsługiwany stan błędu i aktualizowały stan interfejsu, zamiast powodować nieobsługiwane odrzucenia.

Przykładowy kod

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

Sprawdzone metody

  • DO formatuje Connection-Allowlist jako pojedynczą listę wewnętrzną RFC 9651 (...) elementów rozdzielonych spacjami. Jeśli niepusty nagłówek Connection-Allowlist nie przejdzie analizy pola strukturalnego najwyższego poziomu (np. pominięto (...) lub użyto pojedynczych cudzysłowów '...'), przeglądarka nie otworzy się z pustą listą dozwolonych i zablokuje wszystkie wychodzące połączenia sieciowe. Użyj Connection-Allowlist: (), jeśli chcesz celowo unieważnić cały dostęp do sieci po wczytaniu (schematy lokalne data:, blob:, about: i filesystem: pozostają dozwolone).
  • POMIŃ ukośnik na końcu wzorców adresów URL obejmujących całe źródło ("https://api.example.com") lub użyj jawnego symbolu wieloznacznego ("https://api.example.com/*"), aby dozwolone były ścieżki podrzędne, takie jak /v1/users. Usuń końcowe ukośniki dodane przez new URL(origin).href przed serializacją wzorców.
  • DODAJ do listy dozwolonych punkty końcowe WebSocket (ws:// i wss://) przy użyciu wzorców "http://..." i "https://..." w Connection-Allowlist, ponieważ przeglądarka normalizuje schematy WebSocket do HTTP/HTTPS przed dopasowaniem.
  • DO include expected external navigation targets (<a href>, <form action>, OAuth/SSO providers, payment checkouts) in a restricted document's allowlist, and remember that the default redirects=block blocks even same-origin 3xx redirects unless ; redirects=allow is specified.
  • NIE umieszczaj wartości parametrów response-origin ani wartości parametrów listy wewnętrznej (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) w cudzysłowie ani nie oddzielaj elementów wewnątrz (...) przecinkami.
  • NIE używaj niestandardowych grup wyrażeń regularnych, takich jak (\d+), ani ścieżek względnych, takich jak "/api/*", we wzorcach adresów URL. Parser usługi sieciowej przeglądarki odrzuca grupy wyrażeń regularnych i wzorce bez schematu i nazwy hosta.
  • NIE polegaj jeszcze na eksperymentalnym atrybucie <iframe connectionAllowlist="..."> ani na wbudowanym nagłówku egzekwowania Allow-Connection-Allowlist-From w środowisku produkcyjnym, ponieważ wbudowane egzekwowanie nie jest domyślnie włączone w przeglądarkach wysyłkowych.

Strategia zastępcza

Ponieważ zasady Connection-Allowlist są egzekwowane w warstwie sieciowej przeglądarki za pomocą nagłówka odpowiedzi HTTP, nie można ich uzupełnić w kodzie JavaScript po stronie klienta. Jeśli Twój cel Baseline obejmuje przeglądarki, które nie obsługują jeszcze Connection-Allowlist, połącz Connection-Allowlist z nagłówkiem Content-Security-Policy (CSP) (oraz elementem <meta http-equiv="Content-Security-Policy"> w przypadku ograniczeń pobierania i pobierania zasobów podrzędnych na poziomie dokumentu, gdy nie można skonfigurować nagłówków odpowiedzi HTTP):

  • Dodatkowa dogłębna obrona: obsługiwane przeglądarki egzekwują obie zasady jednocześnie – Connection-Allowlist blokuje kanały eksfiltracji, które nie są objęte zasadami CSP (dns-prefetch, preconnect, WebRTC i przekierowania), a zasady CSP ograniczają fetch(), zasoby podrzędne i nawigację <form action> w przeglądarkach bez Connection-Allowlist.
  • Uwzględnij form-action i worker-src: w CSP default-src nie zastępuje <form action>; zawsze uwzględniaj form-action. Uwzględnij worker-src 'self' (z wyjątkiem blob:), aby strony z ograniczeniami nie mogły tworzyć SharedWorkers blob: w celu obejścia ograniczeń na poziomie dokumentu.
  • Mapowanie dozwolonych źródeł na dyrektywy CSP dotyczące poszczególnych zasobów: w przeciwieństwie do Connection-Allowlist (który używa jednej listy punktów końcowych i nie zarządza znacznikami wbudowanymi) default-src 'self' w CSP ogranicza każdy typ zasobu (connect-src, img-src, style-src, font-src, script-src) i blokuje wbudowane bloki <style> i <script>, chyba że są autoryzowane za pomocą skrótów lub liczb jednorazowych.
  • Uwzględnij wss:// w CSP connect-src: Connection-Allowlist dopasowuje połączenia wss:// do wzorców "https://...", ale CSP connect-src w środowiskach z różnymi przeglądarkami powinien wyraźnie wymieniać źródła wss://, gdy używane są 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'"
/>