Bảo mật tài nguyên phụ bên ngoài bằng tính năng Tính toàn vẹn của tài nguyên phụ, CORS và Chính sách liên kết giới thiệu

Khi tải tập lệnh, biểu định kiểu, phông chữ hoặc hình ảnh từ CDN của bên thứ ba hoặc nguồn gốc bên ngoài, hãy xác minh tính toàn vẹn của mật mã bằng tính năng Tính toàn vẹn của tài nguyên phụ (integrity trong HTML và integrity() trong CSS url()), định cấu hình cách xử lý thông tin xác thực trên nhiều nguồn gốc (crossorigin và cross-origin()) và hạn chế tiêu đề Referer gửi đi (referrerpolicy và referrer-policy()). Để biết Chính sách bảo mật nội dung ở cấp tài liệu và danh sách cho phép kết nối gửi đi, hãy xem phần Bảo mật web và Hạn chế kết nối mạng gửi đi bằng Danh sách cho phép kết nối.

1. Tạo thông báo tóm tắt về tính toàn vẹn của mật mã cho các thành phần không thể thay đổi

Tính toàn vẹn của tài nguyên phụ (SRI) đảm bảo trình duyệt chỉ thực thi hoặc hiển thị một thành phần bên ngoài nếu byte của thành phần đó khớp với giá trị băm mật mã do ứng dụng của bạn khai báo.

  • Tính toán chuỗi đại diện tại thời điểm tạo bản dựng hoặc phát hành, không bao giờ tính toán từ các phản hồi trực tiếp không đáng tin cậy: Tạo hàm băm toàn vẹn trong bước tạo bản dựng hoặc phát hành từ các byte tài sản cục bộ đã biết là tốt (hoặc sao chép chuỗi đại diện chính thức do tác giả gói phát hành) và cố định giá trị trong mã vào HTML, CSS hoặc JavaScript của bạn. Không bao giờ tìm nạp URL của bên thứ ba trong trình duyệt trong thời gian chạy để tính toán hàm băm integrity của riêng URL đó, vì việc này sẽ tin tưởng bất kỳ byte nào mà máy chủ từ xa trả về và làm mất tác dụng của SRI.
  • Sử dụng sha384- theo mặc định: Định dạng siêu dữ liệu về tính toàn vẹn dưới dạng <algorithm>-<base64-digest> bằng sha384- (mặc định được đề xuất), sha512- hoặc sha256-.
  • Ghim tài nguyên phụ bên ngoài vào URL không thể thay đổi, có phiên bản: Luôn tham chiếu tài sản của bên thứ ba bằng URL không thể thay đổi, được ghim phiên bản hoặc được định địa chỉ nội dung (chẳng hạn như https://cdn.example.com/ui@2.4.0/app.min.js) thay vì URL động có thể thay đổi (latest/app.min.js), khiến trang web của bạn gặp phải những thay đổi thầm lặng trong chuỗi cung ứng và làm hỏng SRI ngay khi tệp từ xa cập nhật. Nếu bạn phải tải một tập lệnh bên thứ ba có thể thay đổi và cập nhật tại chỗ (chẳng hạn như thẻ phân tích của nhà cung cấp), hãy bỏ qua integrity trên URL đó và hạn chế URL đó bằng Chính sách bảo mật nội dung (strict-dynamic/nonce) hoặc hộp cát iframe.
  • Phân tách nhiều chuỗi băm bằng dấu cách khi một điểm cuối có thể phân phát nhiều tải trọng hợp lệ: Tất cả trình duyệt hỗ trợ SRI đều triển khai sha256-, sha384- và sha512-. Nếu một URL có thể trả về một cách hợp pháp một trong số nhiều tải trọng hợp lệ đã biết (chẳng hạn như trong quá trình thử nghiệm A/B, triển khai CDN theo giai đoạn hoặc bản dựng được thương lượng nội dung), hãy liệt kê nhiều chuỗi đại diện của cùng một thuật toán, phân tách bằng dấu cách (ví dụ: integrity="sha384-variantA... sha384-variantB..."). Trình duyệt sẽ chấp nhận phản hồi nếu phản hồi đó khớp với bất kỳ chuỗi đại diện nào được liệt kê của thuật toán mạnh nhất hiện có.
  • Phân phát tài nguyên phụ trên nhiều nguồn gốc bằng tiêu đề CORS: Trình duyệt chỉ có thể xác minh tính toàn vẹn của phản hồi trên nhiều nguồn gốc khi phản hồi đó có thể đọc được bằng CORS (Access-Control-Allow-Origin: * cho các yêu cầu ẩn danh hoặc nguồn gốc yêu cầu chính xác bằng Access-Control-Allow-Credentials: true cho các yêu cầu có thông tin xác thực).
# Generate a sha384- SRI hash at build time from a known-good local file
printf "sha384-%s\n" "$(openssl dgst -sha384 -binary app.min.js | openssl base64 -A)"
// Build-time / tooling helper (Node.js 19+, Deno, Bun) to compute an SRI string from known-good local bytes
export async function computeIntegrityMetadata(bytes) {
  const rawBytes =
    typeof bytes === 'string' ? new TextEncoder().encode(bytes) : bytes;
  const digestBuffer = await crypto.subtle.digest('SHA-384', rawBytes);
  const binary = Array.from(new Uint8Array(digestBuffer), (b) =>
    String.fromCharCode(b),
  ).join('');
  return `sha384-${btoa(binary)}`;
}

2. Bảo mật tập lệnh HTML, biểu định kiểu và tải trước

Đặt tiêu đề phản hồi HTTP Referrer-Policy: strict-origin-when-cross-origin ở cấp tài liệu (hoặc thêm <meta name="referrer" content="strict-origin-when-cross-origin"> vào <head>) làm giá trị mặc định cơ sở để mọi tài nguyên phụ hoặc url() CSS bỏ qua đối tượng sửa đổi đường liên kết giới thiệu theo yêu cầu vẫn loại bỏ các tham số truy vấn và đường dẫn nhạy cảm trong các yêu cầu khác nguồn gốc.

Sau đó, trên các phần tử <script>, <link rel="stylesheet">, <link rel="preload"> và <link rel="modulepreload"> bên ngoài, hãy luôn đặt integrity, crossorigin và referrerpolicy cùng nhau:

  • integrity="sha384-...": Chặn việc thực thi hoặc áp dụng tài nguyên nếu byte phản hồi không khớp với thông báo dự kiến (đối với <link rel="stylesheet"> bị chặn, linkElement.sheet vẫn là null và sự kiện error kích hoạt).
  • crossorigin="anonymous" (mặc định được đề xuất cho các thành phần CDN công khai) hoặc crossorigin="use-credentials": Bắt buộc bất cứ khi nào integrity được đặt trên một tài nguyên khác nguồn gốc. Việc bỏ qua crossorigin sẽ khiến trình duyệt tìm nạp tài nguyên ở chế độ no-cors (tạo ra phản hồi mờ không thể xác minh bằng hàm băm) và chặn tài nguyên.
  • referrerpolicy="no-referrer" (mặc định được đề xuất cho CDN của bên thứ ba) hoặc referrerpolicy="strict-origin-when-cross-origin": Ghi đè chính sách liên kết giới thiệu tài liệu để ngăn chặn việc rò rỉ bất kỳ thông tin nào về nguồn gốc hoặc đường dẫn trong tiêu đề của yêu cầu Referer đến máy chủ lưu trữ của bên thứ ba.
<!-- Document-level baseline referrer policy in <head> (or via Referrer-Policy HTTP header) -->
<meta name="referrer" content="strict-origin-when-cross-origin" />

<!-- External stylesheet with SRI, anonymous CORS, and no referrer -->
<link
  rel="stylesheet"
  href="https://cdn.example.com/ui@2.4.0/styles.css"
  integrity="sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
/>

<!-- External script with SRI, anonymous CORS, and no referrer -->
<script
  src="https://cdn.example.com/ui@2.4.0/bundle.min.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
  crossorigin="anonymous"
  referrerpolicy="no-referrer"
></script>

Khi tìm nạp tài nguyên phụ bên ngoài theo phương thức lập trình bằng fetch(), hãy truyền các tuỳ chọn integrity, mode, credentials và referrerPolicy phù hợp (nếu thông báo tóm tắt không khớp, lời hứa sẽ bị từ chối bằng TypeError):

const response = await fetch('https://cdn.example.com/ui@2.4.0/manifest.json', {
  integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC',
  mode: 'cors',
  credentials: 'omit',
  referrerPolicy: 'no-referrer',
});

3. Bảo mật hình ảnh, phông chữ và biểu định kiểu đã nhập của CSS bằng đối tượng sửa đổi url()

Đối tượng sửa đổi yêu cầu CSS url() (cross-origin(), referrer-policy() và integrity()) kiểm soát chế độ tìm nạp, tiêu đề liên kết giới thiệu và quy trình xác minh Tính toàn vẹn của tài nguyên phụ đối với các tài nguyên do CSS tải (bao gồm cả các quy tắc background-image, content, mask-image, border-image-source, filter, @font-face src và @import) ngay trong CSS:

Hàm sửa đổi Cú pháp đối số và giá trị được phép Mục đích
cross-origin(<ident>) Từ khoá không được đặt trong dấu ngoặc kép: anonymous (mặc định được đề xuất) hoặc use-credentials Tìm nạp tài nguyên CSS bằng CORS và kiểm soát việc có gửi thông tin đăng nhập hay không. Luôn thêm cùng với integrity() trên tài nguyên CSS khác nguồn gốc.
referrer-policy(<ident>) Từ khoá không được đặt trong dấu ngoặc kép: no-referrer (mặc định được đề xuất cho thành phần của bên thứ ba), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade hoặc unsafe-url Ghi đè chính sách liên kết giới thiệu tài liệu cho yêu cầu CSS cụ thể này.
integrity("<string>") Chuỗi được trích dẫn: "sha384-<base64>" (hoặc chuỗi tóm tắt được phân tách bằng dấu cách "sha384-... sha384-...") Xác minh tài nguyên CSS đã tìm nạp dựa trên thông báo tóm tắt mật mã dự kiến trước khi hiển thị hoặc áp dụng tài nguyên đó.

Hãy tuân theo các quy tắc bảo mật và cú pháp CSS sau đây khi tạo đối tượng sửa đổi url():

  • Đặt chuỗi URL trong dấu ngoặc kép bên trong url("..."): Đối tượng sửa đổi yêu cầu chỉ hợp lệ khi URL bên trong url(...) là một <string> được đặt trong dấu ngoặc kép (URL không được đặt trong dấu ngoặc kép không thể sử dụng đối tượng sửa đổi).
  • Chỉ định mỗi loại đối tượng sửa đổi tối đa một lần cho mỗi url(): Bạn có thể kết hợp cross-origin(), referrer-policy() và integrity() theo thứ tự bất kỳ bên trong url("..." ...), nhưng việc lặp lại cùng một loại đối tượng sửa đổi hai lần (chẳng hạn như hai đối tượng sửa đổi integrity()) là không hợp lệ. Để cung cấp nhiều hàm băm, hãy phân tách chúng bằng dấu cách trong một chuỗi integrity("sha384-... sha384-...") duy nhất.
  • Luôn ghép nối integrity() khác nguồn gốc với cross-origin(anonymous): Mặc dù @font-face src và mask-image đã tìm nạp ở chế độ cors ẩn danh theo mặc định, nhưng background-image, content, border-image-source và @import mặc định ở chế độ no-cors (tạo ra phản hồi mờ không thể xác minh bằng hàm băm) và SVG filter bên ngoài mặc định ở chế độ same-origin (chặn hoàn toàn các URL khác nguồn gốc). Việc thêm cross-origin(anonymous) (hoặc cross-origin(use-credentials)) đảm bảo quy trình xác minh CORS nhất quán trên mọi thuộc tính CSS.
  • Đặt đối tượng sửa đổi bên trong url("...") trong quy tắc @import và @font-face: Giữ cross-origin(), referrer-policy() và integrity() bên trong url("..." ...), đồng thời giữ các chỉ số mô tả phông chữ như format("woff2") và tech(...) bên ngoài url(...).
@import url(
  "https://cdn.example.com/theme@1.2.0/tokens.css"
  cross-origin(anonymous)
  referrer-policy(no-referrer)
  integrity("sha384-bs0EksOYBbU/g0IAHMXbSyH1Mgw83GKn+EZlSI+VE+zVchXmSbLwhVLrPzsOWnc5")
);

@font-face {
  font-family: "BrandSans";
  src: url(
    "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
  ) format("woff2");
  font-display: swap;
}

.verified-graphic {
  background-image: url(
    "https://cdn.example.com/media@1.0.0/badge.svg"
    cross-origin(anonymous)
    referrer-policy(no-referrer)
    integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
  );
}

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

Nếu mục tiêu Baseline của bạn chưa hỗ trợ các đối tượng sửa đổi yêu cầu CSS url() (cross-origin(), referrer-policy() hoặc integrity()), hãy kết hợp các chính sách cấp tài liệu với các nội dung khai báo CSS xếp tầng để các tài nguyên phụ vẫn hoạt động và được bảo vệ:

  1. Ưu tiên HTML <link rel="stylesheet"> hơn CSS @import khi cần có SRI trên nhiều trình duyệt: Vì subresource-integrity và referrer-policy trên <link rel="stylesheet"> được cung cấp rộng rãi trên tất cả trình duyệt, hãy tải biểu định kiểu bên ngoài bằng <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> thay vì @import bất cứ khi nào bạn phải thực thi quy trình xác minh tính toàn vẹn trên mọi trình duyệt.
  2. Dựa vào tiêu đề Referrer-Policy hoặc <meta name="referrer"> ở cấp tài liệu từ Bước 2: Trong các trình duyệt không hỗ trợ referrer-policy() bên trong CSS url(), phần tử Referrer-Policy: strict-origin-when-cross-origin hoặc <meta name="referrer" content="strict-origin-when-cross-origin"> ở cấp tài liệu sẽ tự động loại bỏ đường dẫn nhạy cảm và tham số truy vấn trên các yêu cầu CSS khác nguồn gốc.
  3. Xếp tầng các nội dung khai báo CSS riêng biệt để cải tiến tăng dần: Các trình duyệt không nhận dạng được đối tượng sửa đổi bên trong url("..." ...) sẽ loại bỏ toàn bộ nội dung khai báo tại thời điểm phân tích cú pháp CSS. Trước tiên, hãy khai báo một url("...") đơn giản, sau đó là khai báo cross-origin() và referrer-policy() (cho các trình duyệt hỗ trợ hai đối tượng sửa đổi đó nhưng không hỗ trợ integrity()), sau đó là khai báo đầy đủ bao gồm integrity("sha384-..."):

    • Tuyệt đối không đặt url() chưa được xác minh vào cùng một danh sách @font-face src được phân tách bằng dấu phẩy: Việc viết src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); trong một chỉ số mô tả src duy nhất sẽ bỏ qua Tính toàn vẹn của tài nguyên phụ trong thời gian chạy. Nếu CDN phân phát một phông chữ bị can thiệp và không vượt qua được quy trình kiểm tra integrity(), thì trình duyệt sẽ coi đó là lỗi tải và tự động quay lại url("font.woff2") thứ hai được phân tách bằng dấu phẩy. Việc sử dụng các nội dung khai báo riêng biệt src: đảm bảo rằng các trình duyệt hỗ trợ sẽ ghi đè nội dung khai báo chưa được xác minh tại thời điểm phân tích cú pháp CSS và chặn các phông chữ bị can thiệp tại thời điểm chạy.
    @font-face {
      font-family: "BrandSans";
      /* 1. Parse-time fallback for browsers without url() modifier support */
      src: url("https://cdn.example.com/fonts@1.0.0/brand-sans.woff2") format("woff2");
      /* 2. Parse-time fallback for browsers supporting cross-origin() and referrer-policy() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      ) format("woff2");
      /* 3. Full protection in browsers supporting cross-origin(), referrer-policy(), and integrity() */
      src: url(
        "https://cdn.example.com/fonts@1.0.0/brand-sans.woff2"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC")
      ) format("woff2");
      font-display: swap;
    }
    
    .verified-graphic {
      background-image: url("https://cdn.example.com/media@1.0.0/badge.svg");
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
      );
      background-image: url(
        "https://cdn.example.com/media@1.0.0/badge.svg"
        cross-origin(anonymous)
        referrer-policy(no-referrer)
        integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91")
      );
    }
    
  4. Phát hiện tính năng hỗ trợ công cụ sửa đổi url() CSS trong JavaScript bằng CSS.supports():

export function getCssUrlModifierSupport() {
  const supportsCrossOriginAndReferrer =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer))',
    );

  const supportsUrlIntegrity =
    typeof CSS !== 'undefined' &&
    typeof CSS.supports === 'function' &&
    CSS.supports(
      'background-image',
      'url("https://example.com/a.svg" cross-origin(anonymous) referrer-policy(no-referrer) integrity("sha384-Iw/0p59dP96nGTzHvf2XIfqcBwBW7RM6JfOij8uZxyg5azfNaOOUW8SQy6H/hU91"))',
    );

  return { supportsCrossOriginAndReferrer, supportsUrlIntegrity };
}