Restringe las conexiones de red salientes con listas de entidades permitidas de conexión

El encabezado de respuesta HTTP Connection-Allowlist restringe un documento o un trabajador para que se comunique solo con una lista explícita de extremos de red. A diferencia de la Política de seguridad del contenido (CSP), que requiere mantener directivas independientes por recurso (connect-src, img-src, script-src, font-src, form-action) y no rige los canales de red especulativos o que no son de recuperación, como <link rel="dns-prefetch">, <link rel="preconnect"> o WebRTC, Connection-Allowlist aplica una sola lista de entidades permitidas de capa de red en todas las conexiones salientes que inicia el contexto (fetch(), XMLHttpRequest, recursos secundarios, navigator.sendBeacon(), <a ping>, navegaciones de nivel superior y de marcos, sugerencias de recursos, reglas de especulación, WebSocket, WebTransport y WebRTC).

Cómo implementarlo

  1. Serializa la lista de entidades permitidas como una lista interna de RFC 9651 (...): Aplica el formato Connection-Allowlist (o Connection-Allowlist-Report-Only) como una sola lista entre paréntesis de elementos separados por espacios. Incluye el token sin comillas response-origin para permitir el origen del documento o del trabajador, seguido de cadenas URLPattern entre comillas dobles para cada extremo externo legítimo al que se conecta o navega el contexto (APIs, CDN de imágenes o fuentes, y destinos de redireccionamiento de OAuth, pagos, <a href> o <form action> salientes). Cuando compiles patrones de forma dinámica a partir de objetos de configuración o URL, normaliza ws:/wss: a http:/https: y quita las barras finales en las URLs de solo origen (porque new URL('https://api.example.com').href agrega /, lo que restringe la coincidencia de URLPattern solo a la ruta raíz /).
  2. Limita el middleware por Sec-Fetch-Dest y abarca los trabajadores: Los navegadores solo respetan Connection-Allowlist en las respuestas de documentos o navegación y en las respuestas de secuencias de comandos de trabajadores (valores de Sec-Fetch-Dest document, iframe, worker, sharedworker y serviceworker). Cuando varíes los encabezados por Sec-Fetch-Dest, incluye Vary: Sec-Fetch-Dest para que las cachés compartidas no entreguen respuestas sin encabezado. Publica una lista de entidades permitidas coincidente en las secuencias de comandos de Dedicated Worker y Shared Worker para que las secuencias de comandos de páginas vulneradas no puedan robar datos a través de worker.postMessage(). Debido a que un Service Worker realiza sus propias recuperaciones de red en caso de errores de caché y precargas de navegación, configura su Connection-Allowlist como un superconjunto de las listas de entidades permitidas de todos los documentos que controla.
  3. Configura los informes de incumplimientos y el lanzamiento en etapas o doble: Define un encabezado Reporting-Endpoints y agrega un parámetro ; report-to=<endpoint-name> sin comillas después del ) de cierre. Puedes enviar solo Connection-Allowlist-Report-Only durante el lanzamiento inicial o enviar ambos Connection-Allowlist (aplicando una lista de entidades permitidas de referencia) y Connection-Allowlist-Report-Only (auditando una lista de entidades permitidas candidata más estricta) en la misma respuesta. Los informes de incumplimiento se entregan con "type": "connection-allowlist" y el cuerpo { url, connection, allowlist, disposition } (donde connection es la URL de destino bloqueada o "webrtc", y disposition es "enforce" o "report").
  4. Ten en cuenta el redireccionamiento predeterminado y el bloqueo de WebRTC, y controla los errores del cliente: De forma predeterminada, Connection-Allowlist bloquea todos los redireccionamientos HTTP de 3xx (redirects=block, incluso cuando se redirecciona entre URLs de la lista de entidades permitidas o del mismo origen) y todo el tráfico de RTCPeerConnection (webrtc=block). Agrega ; redirects=allow solo cuando los extremos de confianza requieran redireccionamientos del servidor (la solicitud inicial aún debe coincidir con la lista de entidades permitidas) o ; webrtc=allow cuando se requiera WebRTC. En el cliente, une las llamadas salientes de fetch() en try...catch y controla los eventos WebSocket error para que las solicitudes rechazadas con TypeError (net::ERR_NETWORK_ACCESS_REVOKED) devuelvan un estado de error controlado y actualicen el estado de la IU en lugar de causar rechazos no controlados.

Ejemplo 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ácticas recomendadas

  • SÍ se debe dar formato a Connection-Allowlist como una sola lista interna de RFC 9651 (...) de elementos separados por espacios. Si un encabezado Connection-Allowlist no vacío no puede analizar el campo estructurado de nivel superior (por ejemplo, si se omite (...) o se usan comillas simples '...'), el navegador falla y se cierra con una lista de entidades permitidas vacía y bloquea todas las conexiones de red salientes. Usa Connection-Allowlist: () cuando quieras revocar de forma intencional todo el acceso a la red después de la carga (se siguen permitiendo los esquemas locales data:, blob:, about: y filesystem:).
  • SÍ omitas la barra diagonal final en los patrones de URL de todo el origen ("https://api.example.com") o usa un comodín explícito ("https://api.example.com/*") para que se permitan las rutas de acceso secundarias como /v1/users. Se quitan las barras finales que agrega new URL(origin).href antes de serializar los patrones.
  • SÍ agrega a la lista de entidades permitidas los extremos WebSocket (ws:// y wss://) con los patrones "http://..." y "https://..." en Connection-Allowlist, ya que el navegador normaliza los esquemas de WebSocket a HTTP/HTTPS antes de la coincidencia.
  • SÍ incluye los destinos de navegación externos esperados (<a href>, <form action>, proveedores de OAuth/SSO, confirmaciones de pago) en la lista de entidades permitidas de un documento restringido, y recuerda que el redirects=block predeterminado bloquea incluso los redireccionamientos 3xx del mismo origen, a menos que se especifique ; redirects=allow.
  • NO encierres entre comillas los valores de los parámetros response-origin ni de las listas internas (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow), ni separes los elementos dentro de (...) con comas.
  • NO uses grupos de expresiones regulares personalizados, como (\d+), ni rutas de acceso relativas, como "/api/*", dentro de los patrones de URL. El analizador de servicios de red del navegador rechaza los grupos y patrones de expresiones regulares sin un esquema y un nombre de host.
  • NO dependas todavía del atributo experimental <iframe connectionAllowlist="..."> ni del encabezado de aplicación de política incorporado Allow-Connection-Allowlist-From en producción, ya que la aplicación de política incorporada no está habilitada de forma predeterminada en los navegadores de envío.

Estrategia de resguardo

Debido a que la Connection-Allowlist se aplica en la capa de red del navegador a través de un encabezado de respuesta HTTP, no se puede rellenar con polyfill en JavaScript del cliente. Si tu objetivo de Baseline incluye navegadores que aún no admiten Connection-Allowlist, vincula Connection-Allowlist con un encabezado Content-Security-Policy (CSP) (y un elemento <meta http-equiv="Content-Security-Policy"> para las restricciones de recuperación y subrecursos a nivel del documento cuando no se pueden configurar los encabezados de respuesta HTTP):

  • Defensa en profundidad adicional: Los navegadores compatibles aplican ambas políticas de forma simultánea: Connection-Allowlist bloquea los canales de robo de datos que no son de CSP (dns-prefetch, preconnect, WebRTC y redireccionamientos), mientras que la CSP restringe fetch(), los recursos secundarios y las navegaciones de <form action> en los navegadores sin Connection-Allowlist.
  • Incluye form-action y worker-src de forma explícita: En la CSP, default-src no recurre a <form action> para los envíos; incluye siempre form-action de forma explícita. Incluye worker-src 'self' (excluyendo blob:) para que las páginas restringidas no puedan generar SharedWorkers de blob: para omitir las restricciones a nivel del documento.
  • Asigna orígenes permitidos a directivas de CSP por recurso: A diferencia de Connection-Allowlist (que usa una sola lista de extremos y no rige el lenguaje de marcado intercalado), default-src 'self' en CSP restringe cada tipo de recurso (connect-src, img-src, style-src, font-src, script-src) y bloquea los bloques <style> y <script> intercalados, a menos que se autoricen a través de hashes o nonces.
  • Incluye wss:// en la CSP connect-src: Si bien Connection-Allowlist compara las conexiones wss:// con los patrones "https://...", la CSP connect-src en entornos de varios navegadores debe enumerar de forma explícita los orígenes de wss:// cuando se usan 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'"
/>