Restringir conexões de rede de saída com listas de permissões de conexão

O cabeçalho de resposta HTTP Connection-Allowlist restringe um documento ou worker para que ele se comunique apenas com uma lista explícita de endpoints de rede. Ao contrário da Política de Segurança de Conteúdo (CSP), que exige a manutenção de diretivas separadas por recurso (connect-src, img-src, script-src, font-src, form-action) e não controla canais de rede especulativos ou que não são de busca, como <link rel="dns-prefetch">, <link rel="preconnect"> ou WebRTC, a Connection-Allowlist aplica uma única lista de permissões na camada de rede em todas as conexões de saída iniciadas pelo contexto (fetch(), XMLHttpRequest, subrecursos, navigator.sendBeacon(), <a ping>, navegações de nível superior e de frame, dicas de recursos, regras de especulação, WebSocket, WebTransport e WebRTC).

Como fazer a implementação

  1. Serialize a lista de permissões como uma lista interna do RFC 9651 (...): formate Connection-Allowlist (ou Connection-Allowlist-Report-Only) como uma única lista entre parênteses de itens separados por espaços. Inclua o token sem aspas response-origin para permitir a origem do documento ou do worker, seguido por strings URLPattern entre aspas duplas para cada endpoint externo legítimo ao qual o contexto se conecta ou navega (APIs, CDNs de imagem/fonte e destinos de redirecionamento de pagamento, OAuth, <a href> ou <form action> de saída). Ao criar padrões dinamicamente com base em objetos de configuração ou URL, normalize ws:/wss: para http:/https: e remova as barras finais em URLs somente de origem (porque new URL('https://api.example.com').href anexa /, o que restringe a correspondência de URLPattern apenas ao caminho raiz /).
  2. Defina o escopo do middleware por Sec-Fetch-Dest e cubra os workers: os navegadores só respeitam Connection-Allowlist em respostas de documentos/navegação e de scripts de worker (valores Sec-Fetch-Dest document, iframe, worker, sharedworker e serviceworker). Ao variar os cabeçalhos por Sec-Fetch-Dest, inclua Vary: Sec-Fetch-Dest para que os caches compartilhados não mostrem respostas sem cabeçalho. Disponibilize uma lista de permissões correspondente em scripts de worker dedicado e compartilhado para que scripts de página comprometidos não possam exfiltrar dados por meio de worker.postMessage(). Como um service worker realiza as próprias buscas de rede em ausências no cache e pré-carregamentos de navegação, configure o Connection-Allowlist dele como um superconjunto das listas de permissões de todos os documentos que ele controla.
  3. Configurar relatórios de violação e lançamento em etapas ou duplo: defina um cabeçalho Reporting-Endpoints e anexe um parâmetro ; report-to=<endpoint-name> sem aspas após o ) de fechamento. É possível enviar apenas Connection-Allowlist-Report-Only durante o lançamento inicial ou enviar ambos Connection-Allowlist (aplicando uma lista de permissões de referência) e Connection-Allowlist-Report-Only (auditando uma lista de permissões de candidatos mais rigorosa) na mesma resposta. Os relatórios de violação são enviados com "type": "connection-allowlist" e corpo { url, connection, allowlist, disposition } (em que connection é o URL de destino bloqueado ou "webrtc", e disposition é "enforce" ou "report").
  4. Considere o redirecionamento padrão e o bloqueio do WebRTC e lide com erros do cliente: por padrão, o Connection-Allowlist bloqueia todos os redirecionamentos HTTP 3xx (redirects=block, mesmo ao redirecionar entre URLs na lista de permissões ou de mesma origem) e todo o tráfego RTCPeerConnection (webrtc=block). Adicione ; redirects=allow somente quando os endpoints confiáveis exigirem redirecionamentos do lado do servidor (a solicitação inicial ainda precisa corresponder à lista de permissões) ou ; webrtc=allow quando o WebRTC for necessário. No cliente, envolva as chamadas de saída fetch() em try...catch e processe os eventos WebSocket error para que as solicitações rejeitadas com TypeError (net::ERR_NETWORK_ACCESS_REVOKED) retornem um estado de erro processado e atualizem o status da interface em vez de causar rejeições não processadas.

Exemplo de código

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

Práticas recomendadas

  • FAÇA a formatação de Connection-Allowlist como uma única lista interna RFC 9651 (...) de itens separados por espaços. Se um cabeçalho Connection-Allowlist não vazio falhar na análise de campo estruturado de nível superior (por exemplo, omitindo (...) ou usando aspas simples '...'), o navegador vai falhar com uma lista de permissões vazia e bloquear todas as conexões de rede de saída. Use Connection-Allowlist: () quando quiser revogar intencionalmente todo o acesso à rede após o carregamento (os esquemas locais data:, blob:, about: e filesystem: permanecem permitidos).
  • OMITA a barra final em padrões de URL de toda a origem ("https://api.example.com") ou use um caractere curinga explícito ("https://api.example.com/*") para que subcaminhos como /v1/users sejam permitidos. Remova as barras finais adicionadas por new URL(origin).href antes de serializar os padrões.
  • É RECOMENDÁVEL colocar na lista de permissões os endpoints WebSocket (ws:// e wss://) usando os padrões "http://..." e "https://..." em Connection-Allowlist, porque o navegador normaliza os esquemas WebSocket para HTTP/HTTPS antes de fazer a correspondência.
  • É RECOMENDÁVEL incluir destinos de navegação externa esperados (<a href>, <form action>, provedores de OAuth/SSO, finalizações de pagamento) na lista de permissões de um documento restrito. Lembre-se de que o redirects=block padrão bloqueia até mesmo redirecionamentos 3xx de mesma origem, a menos que ; redirects=allow seja especificado.
  • NÃO coloque response-origin ou valores de parâmetro de lista interna (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) entre aspas nem separe itens dentro de (...) com vírgulas.
  • NÃO use grupos de expressões regulares personalizados, como (\d+), ou caminhos relativos, como "/api/*", em padrões de URL. O analisador de serviço de rede do navegador rejeita grupos e padrões de regex sem um esquema e um nome de host.
  • NÃO use o atributo experimental <iframe connectionAllowlist="..."> ou o cabeçalho de aplicação incorporado Allow-Connection-Allowlist-From na produção, porque a aplicação incorporada não está ativada por padrão nos navegadores de envio.

Estratégia de fallback

Como o Connection-Allowlist é aplicado na camada de rede do navegador por um cabeçalho de resposta HTTP, não é possível usar um polyfill no JavaScript do lado do cliente. Se o seu Baseline incluir navegadores que ainda não oferecem suporte a Connection-Allowlist, pareie Connection-Allowlist com um cabeçalho Content-Security-Policy (CSP) (e um elemento <meta http-equiv="Content-Security-Policy"> para busca no nível do documento e restrições de subrecursos quando os cabeçalhos de resposta HTTP não puderem ser configurados):

  • Defesa em profundidade aditiva: os navegadores compatíveis aplicam as duas políticas simultaneamente. O Connection-Allowlist bloqueia canais de exfiltração que não são da CSP (dns-prefetch, preconnect, WebRTC e redirecionamentos), enquanto a CSP restringe as navegações de fetch(), subrecursos e <form action> em navegadores sem Connection-Allowlist.
  • Cubra form-action e worker-src explicitamente: na CSP, default-src não volta para envios de <form action>. Sempre inclua form-action explicitamente. Inclua worker-src 'self' (exceto blob:) para que as páginas restritas não possam gerar SharedWorkers blob: para ignorar as restrições no nível do documento.
  • Mapear origens permitidas para diretivas de CSP por recurso: ao contrário de Connection-Allowlist (que usa uma única lista de endpoints e não controla a marcação inline), default-src 'self' na CSP restringe cada tipo de recurso (connect-src, img-src, style-src, font-src, script-src) e bloqueia blocos inline <style> e <script>, a menos que sejam autorizados por hashes ou nonces.
  • Incluir wss:// na CSP connect-src: embora o Connection-Allowlist corresponda às conexões wss:// com os padrões "https://...", a CSP connect-src em ambientes de vários navegadores precisa listar explicitamente as origens wss:// quando os WebSockets são usados.
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'"
/>