接続許可リストを使用してアウトバウンド ネットワーク接続を制限する

ウェブ機能: 接続許可リスト

Connection-Allowlist HTTP レスポンス ヘッダーは、ドキュメントまたはワーカーが明示的なネットワーク エンドポイントのリストとのみ通信するように制限します。コンテンツ セキュリティ ポリシー(CSP)では、リソースごとに個別のディレクティブ(connect-src、img-src、script-src、font-src、form-action)を維持する必要があり、<link rel="dns-prefetch">、<link rel="preconnect">、WebRTC などの投機的または非 Fetch ネットワーク チャネルは管理されません。これとは異なり、Connection-Allowlist では、コンテキスト(fetch()、XMLHttpRequest、サブリソース、navigator.sendBeacon()、<a ping>、トップレベルとフレームのナビゲーション、リソースヒント、投機ルール、WebSocket、WebTransport、WebRTC)によって開始されるすべてのアウトバウンド接続に、単一のネットワーク レイヤの許可リストが適用されます。

導入方法

  1. 許可リストを RFC 9651 内部リスト (...) としてシリアル化する: Connection-Allowlist(または Connection-Allowlist-Report-Only)を、スペース区切りのアイテムの単一の括弧付きリストとしてフォーマットします。ドキュメントまたはワーカー独自のオリジンを許可するには、引用符で囲まれていないトークン response-origin を含めます。その後に、コンテキストが接続または移動するすべての正当な外部エンドポイント(API、画像 / フォント CDN、アウトバウンド <a href>、<form action>、OAuth、支払いリダイレクト ターゲット)を、二重引用符で囲まれた URLPattern 文字列として指定します。構成または URL オブジェクトからパターンを動的に構築する場合は、ws:/wss: を http:/https: に正規化し、オリジンのみの URL の末尾のスラッシュを削除します(new URL('https://api.example.com').href は / を追加するため、URLPattern の一致はルートパス / のみに制限されます)。
  2. Sec-Fetch-Dest でミドルウェアをスコープ設定し、ワーカーをカバーする: ブラウザは、ドキュメント / ナビゲーション レスポンスとワーカー スクリプト レスポンス(Sec-Fetch-Dest 値 document、iframe、worker、sharedworker、serviceworker)でのみ Connection-Allowlist を尊重します。Sec-Fetch-Dest でヘッダーを変更する場合は、共有キャッシュがヘッダーのないレスポンスを返さないように Vary: Sec-Fetch-Dest を含めます。専用ワーカー スクリプトと共有ワーカー スクリプトで一致する許可リストを提供し、不正使用されたページ スクリプトが worker.postMessage() を介してデータを引き出せないようにします。Service Worker は、キャッシュミスとナビゲーション プリロードで独自のネットワーク フェッチを実行するため、制御するすべてのドキュメントの許可リストのスーパーセットとして Connection-Allowlist を構成します。
  3. 違反レポートと段階的または二重のロールアウトを構成する: Reporting-Endpoints ヘッダーを定義し、閉じ括弧 ) の後に引用符なしの ; report-to=<endpoint-name> パラメータを追加します。初期ロールアウト時に Connection-Allowlist-Report-Only のみを送信することも、同じレスポンスで Connection-Allowlist(ベースラインの許可リストを適用)と Connection-Allowlist-Report-Only(より厳格な候補許可リストを監査)の両方を送信することもできます。違反レポートは、"type": "connection-allowlist" と本文 { url, connection, allowlist, disposition } で配信されます(connection はブロックされたリンク先 URL または "webrtc"、disposition は "enforce" または "report")。
  4. デフォルトのリダイレクトと WebRTC のブロックを考慮し、クライアント エラーを処理する: デフォルトでは、Connection-Allowlist はすべての 3xx HTTP リダイレクト(redirects=block、許可リストに登録された URL 間または同一オリジン URL 間でリダイレクトする場合でも)とすべての RTCPeerConnection トラフィック(webrtc=block)をブロックします。信頼できるエンドポイントでサーバーサイドのリダイレクトが必要な場合にのみ ; redirects=allow を追加します(最初のリクエストは許可リストと一致する必要があります)。WebRTC が必要な場合は ; webrtc=allow を追加します。クライアントで、送信 fetch() 呼び出しを try...catch でラップし、WebSocket error イベントを処理します。これにより、TypeError(net::ERR_NETWORK_ACCESS_REVOKED)で拒否されたリクエストは、未処理の拒否を引き起こすのではなく、処理済みのエラー状態を返し、UI ステータスを更新します。

コード例

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

ベスト プラクティス

  • 推奨: Connection-Allowlist は、スペース区切りのアイテムの単一の RFC 9651 内部リスト (...) としてフォーマットします。空でない Connection-Allowlist ヘッダーが最上位の構造化フィールドの解析に失敗した場合((...) を省略した場合や、一重引用符 '...' を使用した場合など)、ブラウザは空の許可リストでフェイルクローズし、すべてのアウトバウンド ネットワーク接続をブロックします。読み込み後にすべてのネットワーク アクセスを意図的に取り消す場合は、Connection-Allowlist: () を使用します(ローカル スキーム data:、blob:、about:、filesystem: は引き続き許可されます)。
  • 推奨: オリジン全体の URL パターン("https://api.example.com")の末尾のスラッシュを省略するか、明示的なワイルドカード("https://api.example.com/*")を使用して、/v1/users などのサブパスを許可します。パターンをシリアル化する前に、new URL(origin).href によって追加された末尾のスラッシュを削除します。
  • Connection-Allowlist で "http://..." と "https://..." のパターンを使用して、WebSocket(ws:// と wss://)のエンドポイントを許可リストに登録してください。ブラウザは、照合前に WebSocket スキームを HTTP / HTTPS に正規化するためです。
  • 制限付きドキュメントの許可リストに、想定される外部ナビゲーション ターゲット(<a href>、<form action>、OAuth / SSO プロバイダ、支払いチェックアウト)を含める。デフォルトの redirects=block では、; redirects=allow が指定されていない限り、同一オリジンの 3xx リダイレクトもブロックされることに注意してください。
  • response-origin または内部リストのパラメータ値(; report-to=conn-reports、; redirects=allow、; webrtc=allow)を引用符で囲んだり、(...) 内の項目をカンマで区切ったりしないでください。
  • URL パターン内で (\d+) などのカスタム正規表現グループや "/api/*" などの相対パスは使用しないでください。ブラウザのネットワーク サービス パーサーは、スキームとホスト名のない正規表現グループとパターンを拒否します。
  • <iframe connectionAllowlist="..."> 属性または Allow-Connection-Allowlist-From 埋め込み型適用ヘッダーは試験運用版であり、埋め込み型適用は出荷されるブラウザでデフォルトで有効になっていないため、本番環境ではまだ使用しないでください。

フォールバック戦略

Connection-Allowlist は HTTP レスポンス ヘッダーを介してブラウザのネットワーク層で適用されるため、クライアントサイドの JavaScript でポリフィルすることはできません。ベースライン ターゲットに Connection-Allowlist をまだサポートしていないブラウザが含まれている場合は、Connection-Allowlist と Content-Security-Policy(CSP)ヘッダーをペアにします(HTTP レスポンス ヘッダーを構成できない場合は、ドキュメント レベルのフェッチとサブリソースの制限に <meta http-equiv="Content-Security-Policy"> 要素を使用します)。

  • 多層防御の追加: 対応ブラウザでは、両方のポリシーが同時に適用されます。Connection-Allowlist は CSP 以外のデータ漏洩チャネル(dns-prefetch、preconnect、WebRTC、リダイレクト)をブロックし、CSP は Connection-Allowlist のないブラウザで fetch()、サブリソース、<form action> ナビゲーションを制限します。
  • form-action と worker-src を明示的にカバーする: CSP では、<form action> の送信に対して default-src はフォールバックしません。常に form-action を明示的に含めてください。worker-src 'self'(blob: を除く)を含めて、制限付きのページが blob: SharedWorker を生成してドキュメント レベルの制限を回避できないようにします。
  • 許可されたオリジンをリソースごとの CSP ディレクティブにマッピングする: Connection-Allowlist(単一のエンドポイント リストを使用し、インライン マークアップを管理しない)とは異なり、CSP の default-src 'self' は各リソースタイプ(connect-src、img-src、style-src、font-src、script-src)を制限し、ハッシュまたはノンスを介して認可されていない限り、インライン <style> ブロックと <script> ブロックをブロックします。
  • CSP connect-src に wss:// を含める: Connection-Allowlist は "https://..." パターンに対して wss:// 接続を照合しますが、クロスブラウザ環境の CSP connect-src では、WebSocket が使用されている場合に wss:// のオリジンを明示的にリストする必要があります。
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'"
/>