Mengamankan subresource eksternal dengan Subresource Integrity, CORS, dan Referrer Policy

Saat memuat skrip, stylesheet, font, atau gambar dari CDN pihak ketiga atau origin eksternal, verifikasi integritas kriptografinya dengan Subresource Integrity (integrity di HTML dan integrity() di CSS url()), konfigurasi penanganan kredensial lintas origin (crossorigin dan cross-origin()), dan batasi header Referer keluar (referrerpolicy dan referrer-policy()). Untuk Kebijakan Keamanan Konten tingkat dokumen dan daftar yang diizinkan untuk koneksi keluar, lihat Keamanan Web dan Membatasi koneksi jaringan keluar dengan Daftar Koneksi yang Diizinkan.

1. Membuat ringkasan integritas kriptografis untuk aset yang tidak dapat diubah

Subresource Integrity (SRI) memastikan browser hanya mengeksekusi atau merender aset eksternal jika byte-nya cocok dengan hash kriptografis yang dideklarasikan oleh aplikasi Anda.

  • Hitung ringkasan pada waktu build atau rilis, jangan pernah dari respons langsung yang tidak tepercaya: Buat hash integritas selama langkah build atau rilis dari byte aset lokal yang diketahui baik (atau salin ringkasan resmi yang dipublikasikan oleh pembuat paket) dan hardcode ke dalam HTML, CSS, atau JavaScript Anda. Jangan pernah mengambil URL pihak ketiga di browser saat runtime untuk menghitung hash integrity-nya sendiri, yang memercayai byte apa pun yang ditampilkan server jarak jauh dan mengabaikan SRI.
  • Gunakan sha384- secara default: Format metadata integritas sebagai <algorithm>-<base64-digest> menggunakan sha384- (default yang direkomendasikan), sha512-, atau sha256-.
  • Sematkan subresource eksternal ke URL yang tidak dapat diubah dan berversi: Selalu referensikan aset pihak ketiga menggunakan URL yang tidak dapat diubah, disematkan versi, atau beralamat konten (seperti https://cdn.example.com/ui@2.4.0/app.min.js), bukan URL mengambang yang dapat diubah (latest/app.min.js), yang mengekspos situs Anda ke perubahan supply chain yang tidak terlihat dan merusak SRI segera setelah file jarak jauh diperbarui. Jika Anda harus memuat skrip pihak ketiga yang dapat diubah dan diperbarui di tempat (seperti tag analisis vendor), hapus integrity di URL tersebut dan batasi dengan Kebijakan Keamanan Konten (strict-dynamic/nonce) atau iframe sandboxing.
  • Pisahkan beberapa ringkasan dengan spasi jika endpoint dapat menyajikan beberapa payload yang valid: Semua browser yang mendukung SRI menerapkan sha256-, sha384-, dan sha512-. Jika URL dapat secara sah menampilkan salah satu dari beberapa payload yang diketahui baik (seperti selama pengujian A/B, peluncuran CDN bertahap, atau build yang dinegosiasikan konten), cantumkan beberapa ringkasan algoritma yang sama yang dipisahkan oleh spasi kosong (misalnya, integrity="sha384-variantA... sha384-variantB..."). Browser akan menerima respons jika cocok dengan setiap ringkasan yang tercantum dari algoritma terkuat yang ada.
  • Menyajikan subresource lintas origin dengan header CORS: Browser hanya dapat memverifikasi integritas respons lintas origin jika respons tersebut dapat dibaca CORS (Access-Control-Allow-Origin: * untuk permintaan anonim, atau origin permintaan yang tepat dengan Access-Control-Allow-Credentials: true untuk permintaan yang menggunakan kredensial).
# 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. Mengamankan skrip, stylesheet, dan pramuat HTML

Tetapkan header respons HTTP Referrer-Policy: strict-origin-when-cross-origin tingkat dokumen (atau sertakan <meta name="referrer" content="strict-origin-when-cross-origin"> di <head>) sebagai default dasar pengukuran sehingga subresource atau url() CSS yang menghilangkan pengubah perujuk per permintaan tetap menghapus parameter jalur dan kueri sensitif pada permintaan lintas origin.

Kemudian, pada elemen <script>, <link rel="stylesheet">, <link rel="preload">, dan <link rel="modulepreload"> eksternal, selalu tetapkan integrity, crossorigin, dan referrerpolicy bersama-sama:

  • integrity="sha384-...": Memblokir eksekusi atau penerapan resource jika byte respons tidak cocok dengan ringkasan yang diharapkan (untuk <link rel="stylesheet"> yang diblokir, linkElement.sheet tetap null dan peristiwa error diaktifkan).
  • crossorigin="anonymous" (default yang direkomendasikan untuk aset CDN publik) atau crossorigin="use-credentials": Diperlukan setiap kali integrity ditetapkan pada resource lintas origin. Menghilangkan crossorigin akan menyebabkan browser mengambil resource dalam mode no-cors (menghasilkan respons buram yang tidak dapat diverifikasi hash-nya) dan memblokir resource.
  • referrerpolicy="no-referrer" (default yang direkomendasikan untuk CDN pihak ketiga) atau referrerpolicy="strict-origin-when-cross-origin": Mengganti kebijakan perujuk dokumen untuk mencegah kebocoran informasi asal atau jalur di header permintaan Referer ke host pihak ketiga.
<!-- 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>

Saat mengambil subresource eksternal secara terprogram dengan fetch(), teruskan opsi integrity, mode, credentials, dan referrerPolicy yang cocok (ketidakcocokan ringkasan akan menolak promise dengan 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. Mengamankan gambar, font, dan stylesheet yang diimpor CSS dengan pengubah url()

Pengubah permintaan url() CSS (cross-origin(), referrer-policy(), dan integrity()) mengontrol mode pengambilan, header perujuk, dan verifikasi Subresource Integrity resource yang dimuat CSS—termasuk aturan background-image, content, mask-image, border-image-source, filter, @font-face src, dan @import—langsung dari CSS:

Fungsi pengubah Sintaksis argumen & nilai yang diizinkan Tujuan
cross-origin(<ident>) Kata kunci tanpa tanda petik: anonymous (default yang direkomendasikan) atau use-credentials Mengambil resource CSS menggunakan CORS dan mengontrol apakah kredensial dikirim. Selalu sertakan bersama integrity() pada resource CSS lintas origin.
referrer-policy(<ident>) Kata kunci tanpa tanda petik: no-referrer (default yang direkomendasikan untuk aset pihak ketiga), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade, atau unsafe-url Mengganti kebijakan perujuk dokumen untuk permintaan CSS spesifik ini.
integrity("<string>") String kutipan: "sha384-<base64>" (atau ringkasan yang dipisahkan spasi "sha384-... sha384-...") Memverifikasi resource CSS yang diambil terhadap ringkasan kriptografis yang diharapkan sebelum merender atau menerapkannya.

Ikuti aturan keamanan dan sintaksis CSS berikut saat menulis pengubah url():

  • Mengutip string URL di dalam url("..."): Pengubah permintaan hanya valid jika URL di dalam url(...) adalah <string> yang dikutip (URL yang tidak dikutip tidak dapat menggunakan pengubah).
  • Tentukan setiap jenis pengubah maksimal sekali per url(): Anda dapat menggabungkan cross-origin(), referrer-policy(), dan integrity() dalam urutan apa pun di dalam url("..." ...), tetapi mengulangi jenis pengubah yang sama dua kali (seperti dua pengubah integrity()) tidak valid. Untuk memberikan beberapa hash, pisahkan dengan spasi di dalam satu string integrity("sha384-... sha384-...").
  • Selalu pasangkan integrity() lintas origin dengan cross-origin(anonymous): Meskipun @font-face src dan mask-image sudah mengambil dalam mode cors anonim secara default, background-image, content, border-image-source, dan @import secara default menggunakan mode no-cors (menghasilkan respons buram yang tidak dapat diverifikasi hash) dan filter SVG eksternal secara default menggunakan mode same-origin (memblokir URL lintas origin secara langsung). Menyertakan cross-origin(anonymous) (atau cross-origin(use-credentials)) akan memastikan verifikasi CORS yang konsisten di setiap properti CSS.
  • Tempatkan pengubah di dalam url("...") dalam aturan @import dan @font-face: Pertahankan cross-origin(), referrer-policy(), dan integrity() di dalam url("..." ...), sambil mempertahankan deskriptor font seperti format("woff2") dan tech(...) di luar 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")
  );
}

Strategi penggantian

Jika target Dasar Pengukuran Anda belum mendukung pengubah permintaan url() CSS (cross-origin(), referrer-policy(), atau integrity()), gabungkan kebijakan tingkat dokumen dengan deklarasi CSS bertingkat sehingga subresource tetap berfungsi dan terlindungi:

  1. Lebih pilih HTML <link rel="stylesheet"> daripada CSS @import jika SRI lintas browser diperlukan: Karena subresource-integrity dan referrer-policy di <link rel="stylesheet"> tersedia secara luas di semua browser, muat stylesheet eksternal dengan <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer">, bukan @import, setiap kali verifikasi integritas harus diterapkan di setiap browser.
  2. Mengandalkan header Referrer-Policy atau <meta name="referrer"> tingkat dokumen dari Langkah 2: Di browser yang tidak mendukung referrer-policy() di dalam url() CSS, header Referrer-Policy: strict-origin-when-cross-origin atau elemen <meta name="referrer" content="strict-origin-when-cross-origin"> tingkat dokumen akan otomatis menghapus parameter jalur dan kueri sensitif pada permintaan CSS lintas origin.
  3. Gabungkan deklarasi CSS terpisah untuk progressive enhancement: Browser yang tidak mengenali pengubah di dalam url("..." ...) akan menghapus seluruh deklarasi pada waktu penguraian CSS. Deklarasikan url("...") biasa terlebih dahulu, diikuti dengan deklarasi cross-origin() dan referrer-policy() (untuk browser yang mendukung kedua pengubah tersebut, tetapi tidak mendukung integrity()), diikuti dengan deklarasi lengkap termasuk integrity("sha384-..."):

    • Jangan pernah menempatkan url() yang belum diverifikasi di dalam daftar @font-face src yang dipisahkan koma yang sama: Menulis src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); dalam satu deskriptor src akan mengabaikan Subresource Integrity saat runtime—jika CDN menyajikan font yang dimodifikasi dan gagal dalam pemeriksaan integrity(), browser akan memperlakukannya sebagai kegagalan pemuatan dan otomatis beralih ke url("font.woff2") kedua yang dipisahkan koma. Menggunakan deklarasi src: terpisah akan memastikan browser pendukung menimpa deklarasi yang tidak diverifikasi pada waktu penguraian CSS dan memblokir font yang diubah pada waktu proses.
    @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. Deteksi fitur dukungan pengubah url() CSS di JavaScript dengan 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 };
}