Hạn chế kết nối mạng đi bằng Danh sách cho phép kết nối

Tiêu đề phản hồi HTTP Connection-Allowlist hạn chế một tài liệu hoặc trình thực thi chỉ giao tiếp với một danh sách rõ ràng các điểm cuối mạng. Không giống như Chính sách bảo mật nội dung (CSP) – yêu cầu duy trì các lệnh riêng biệt cho mỗi tài nguyên (connect-src, img-src, script-src, font-src, form-action) và không quản lý các kênh mạng suy đoán hoặc không phải Fetch như <link rel="dns-prefetch">, <link rel="preconnect"> hoặc WebRTC – Connection-Allowlist thực thi một danh sách cho phép duy nhất ở lớp mạng trên tất cả kết nối đi do ngữ cảnh khởi tạo (fetch(), XMLHttpRequest, tài nguyên phụ, navigator.sendBeacon(), <a ping>, thao tác điều hướng cấp cao nhất và khung, gợi ý tài nguyên, Quy tắc suy đoán, WebSocket, WebTransport và WebRTC).

Cách triển khai

  1. Chuyển đổi danh sách cho phép thành Danh sách nội bộ RFC 9651 (...): Định dạng Connection-Allowlist (hoặc Connection-Allowlist-Report-Only) dưới dạng một danh sách duy nhất trong dấu ngoặc đơn gồm các mục được phân tách bằng dấu cách. Thêm mã thông báo không được trích dẫn response-origin để cho phép nguồn gốc của tài liệu hoặc trình chạy, theo sau là chuỗi URLPattern được trích dẫn kép cho mọi điểm cuối bên ngoài hợp pháp mà ngữ cảnh kết nối hoặc điều hướng đến (API, CDN hình ảnh/phông chữ và mục tiêu chuyển hướng <a href>, <form action>, OAuth hoặc thanh toán ra bên ngoài). Khi tạo mẫu một cách linh động từ cấu hình hoặc đối tượng URL, hãy chuẩn hoá ws:/wss: thành http:/https: và loại bỏ dấu gạch chéo ở cuối URL chỉ có nguồn gốc (vì new URL('https://api.example.com').href thêm /, điều này hạn chế việc so khớp URLPattern chỉ với đường dẫn gốc /).
  2. Phạm vi phần mềm trung gian theo Sec-Fetch-Dest và bao gồm cả worker: Trình duyệt chỉ tuân thủ Connection-Allowlist trên các phản hồi tài liệu/điều hướng và phản hồi tập lệnh worker (giá trị Sec-Fetch-Dest document, iframe, worker, sharedworker và serviceworker). Khi thay đổi tiêu đề theo Sec-Fetch-Dest, hãy thêm Vary: Sec-Fetch-Dest để bộ nhớ đệm dùng chung không phân phát phản hồi không có tiêu đề. Cung cấp danh sách cho phép phù hợp trên tập lệnh Worker chuyên dụng và Worker dùng chung để tập lệnh trang bị xâm phạm không thể lấy dữ liệu thông qua worker.postMessage(). Vì Trình chạy dịch vụ thực hiện các hoạt động tìm nạp mạng riêng khi thiếu bộ nhớ đệm và tải trước thành phần điều hướng, hãy định cấu hình Connection-Allowlist của trình chạy dịch vụ dưới dạng một superset của danh sách cho phép của tất cả tài liệu mà trình chạy dịch vụ kiểm soát.
  3. Định cấu hình báo cáo lỗi vi phạm và triển khai theo giai đoạn hoặc triển khai kép: Xác định tiêu đề Reporting-Endpoints và thêm tham số ; report-to=<endpoint-name> không có dấu ngoặc kép sau dấu đóng ). Bạn có thể chỉ gửi Connection-Allowlist-Report-Only trong lần triển khai đầu tiên hoặc gửi cả Connection-Allowlist (thực thi danh sách cho phép cơ sở) và Connection-Allowlist-Report-Only (kiểm tra danh sách cho phép nghiêm ngặt hơn) trên cùng một phản hồi. Báo cáo lỗi vi phạm được gửi kèm theo "type": "connection-allowlist" và nội dung { url, connection, allowlist, disposition } (trong đó connection là URL đích bị chặn hoặc "webrtc", còn disposition là "enforce" hoặc "report").
  4. Tính đến việc chặn chuyển hướng mặc định và WebRTC, đồng thời xử lý lỗi ứng dụng: Theo mặc định, Connection-Allowlist chặn tất cả lệnh chuyển hướng HTTP 3xx (redirects=block, ngay cả khi chuyển hướng giữa các URL được đưa vào danh sách cho phép hoặc URL gốc) và tất cả lưu lượng truy cập RTCPeerConnection (webrtc=block). Chỉ thêm ; redirects=allow khi các điểm cuối đáng tin cậy yêu cầu chuyển hướng phía máy chủ (yêu cầu ban đầu vẫn phải khớp với danh sách cho phép) hoặc ; webrtc=allow khi cần có WebRTC. Trên ứng dụng, hãy gói các lệnh gọi fetch() gửi đi trong try...catch và xử lý các sự kiện WebSocket error để các yêu cầu bị từ chối bằng TypeError (net::ERR_NETWORK_ACCESS_REVOKED) trả về trạng thái lỗi đã xử lý và cập nhật trạng thái giao diện người dùng thay vì gây ra các trường hợp từ chối chưa xử lý.

Mã mẫu

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

Các phương pháp hay nhất

  • NÊN định dạng Connection-Allowlist dưới dạng một Danh sách bên trong RFC 9651 (...) gồm các mục được phân tách bằng dấu cách. Nếu tiêu đề Connection-Allowlist không trống không phân tích cú pháp được Trường có cấu trúc cấp cao nhất (ví dụ: bỏ qua (...) hoặc sử dụng dấu nháy đơn '...'), thì trình duyệt sẽ không đóng được bằng danh sách cho phép trống và chặn tất cả kết nối mạng đi. Sử dụng Connection-Allowlist: () khi bạn muốn thu hồi tất cả quyền truy cập mạng sau khi tải (các lược đồ cục bộ data:, blob:, about: và filesystem: vẫn được cho phép).
  • NÊN bỏ dấu gạch chéo ở cuối mẫu URL trên toàn bộ nguồn gốc ("https://api.example.com") hoặc sử dụng ký tự đại diện rõ ràng ("https://api.example.com/*") để cho phép các đường dẫn phụ như /v1/users. Xoá dấu gạch chéo ở cuối do new URL(origin).href thêm vào trước khi chuyển đổi tuần tự các mẫu.
  • NÊN đưa các điểm cuối WebSocket (ws:// và wss://) vào danh sách cho phép bằng cách sử dụng các mẫu "http://..." và "https://..." trong Connection-Allowlist, vì trình duyệt sẽ chuẩn hoá các lược đồ WebSocket thành HTTP/HTTPS trước khi so khớp.
  • NÊN đưa các mục tiêu điều hướng bên ngoài dự kiến (<a href>, <form action>, nhà cung cấp OAuth/SSO, quy trình thanh toán) vào danh sách cho phép của tài liệu bị hạn chế và hãy nhớ rằng redirects=block mặc định sẽ chặn ngay cả các lệnh chuyển hướng 3xx cùng nguồn gốc, trừ phi bạn chỉ định ; redirects=allow.
  • KHÔNG gói các giá trị tham số response-origin hoặc danh sách bên trong (; report-to=conn-reports, ; redirects=allow, ; webrtc=allow) trong dấu ngoặc kép hoặc phân tách các mục bên trong (...) bằng dấu phẩy.
  • KHÔNG sử dụng các nhóm biểu thức chính quy tuỳ chỉnh như (\d+) hoặc đường dẫn tương đối như "/api/*" bên trong mẫu URL; trình phân tích cú pháp dịch vụ mạng của trình duyệt sẽ từ chối các nhóm và mẫu biểu thức chính quy không có lược đồ và tên máy chủ.
  • KHÔNG dựa vào thuộc tính <iframe connectionAllowlist="..."> thử nghiệm hoặc tiêu đề thực thi được nhúng Allow-Connection-Allowlist-From trong phiên bản chính thức, vì tính năng thực thi được nhúng không được bật theo mặc định trong các trình duyệt vận chuyển.

Chiến lược dự phòng

Vì Connection-Allowlist được thực thi ở tầng mạng trình duyệt thông qua tiêu đề phản hồi HTTP, nên không thể polyfill trong JavaScript phía máy khách. Nếu mục tiêu Baseline của bạn bao gồm các trình duyệt chưa hỗ trợ Connection-Allowlist, hãy ghép nối Connection-Allowlist với tiêu đề Content-Security-Policy (CSP) (và phần tử <meta http-equiv="Content-Security-Policy"> cho các quy định hạn chế về tài nguyên phụ và tìm nạp ở cấp tài liệu khi không thể định cấu hình tiêu đề phản hồi HTTP):

  • Phòng thủ theo chiều sâu bổ sung: Các trình duyệt hỗ trợ thực thi cả hai chính sách cùng một lúc – Connection-Allowlist chặn các kênh rò rỉ không phải CSP (dns-prefetch, preconnect, WebRTC và lệnh chuyển hướng), trong khi CSP hạn chế fetch(), tài nguyên phụ và thao tác điều hướng <form action> trong các trình duyệt không có Connection-Allowlist.
  • Bao gồm form-action và worker-src một cách rõ ràng: Trong CSP, default-src không dự phòng cho các lần gửi <form action>; luôn bao gồm form-action một cách rõ ràng. Thêm worker-src 'self' (ngoại trừ blob:) để các trang bị hạn chế không thể tạo SharedWorker blob: nhằm vượt qua các quy định hạn chế ở cấp tài liệu.
  • Liên kết các nguồn gốc được phép với các lệnh CSP cho mỗi tài nguyên: Không giống như Connection-Allowlist (sử dụng một danh sách điểm cuối duy nhất và không quản lý mã đánh dấu cùng dòng), default-src 'self' trong CSP hạn chế từng loại tài nguyên (connect-src, img-src, style-src, font-src, script-src) và chặn các khối <style> và <script> cùng dòng, trừ phi được uỷ quyền thông qua hàm băm hoặc số chỉ dùng một lần.
  • Thêm wss:// vào CSP connect-src: Mặc dù Connection-Allowlist so khớp các kết nối wss:// với mẫu "https://...", nhưng CSP connect-src trong môi trường trên nhiều trình duyệt phải liệt kê rõ ràng các nguồn gốc wss:// khi sử dụng WebSocket.
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'"
/>