รักษาความปลอดภัยของทรัพยากรย่อยภายนอกด้วยความสมบูรณ์ของทรัพยากรย่อย, CORS และนโยบายผู้อ้างอิง

เมื่อโหลดสคริปต์ สไตล์ชีต แบบอักษร หรือรูปภาพจาก CDN ของบุคคลที่สามหรือต้นทางภายนอก ให้ตรวจสอบความสมบูรณ์ของการเข้ารหัสด้วย Subresource Integrity (integrity ใน HTML และ integrity() ใน CSS url()) กำหนดค่าการจัดการข้อมูลเข้าสู่ระบบแบบข้ามต้นทาง (crossorigin และ cross-origin()) และจำกัดส่วนหัว Referer ขาออก (referrerpolicy และ referrer-policy()) สำหรับนโยบายรักษาความปลอดภัยเนื้อหาระดับเอกสารและรายการที่อนุญาตสำหรับการเชื่อมต่อขาออก โปรดดูความปลอดภัยบนเว็บและจำกัดการเชื่อมต่อเครือข่ายขาออกด้วยรายการที่อนุญาตสำหรับการเชื่อมต่อ

1. สร้างไดเจสต์ความสมบูรณ์ของการเข้ารหัสสำหรับเนื้อหาที่เปลี่ยนแปลงไม่ได้

Subresource Integrity (SRI) ช่วยให้มั่นใจว่าเบราว์เซอร์จะเรียกใช้หรือแสดงผลเนื้อหาภายนอกก็ต่อเมื่อไบต์ของเนื้อหานั้นตรงกับแฮชแบบเข้ารหัสที่แอปพลิเคชันของคุณประกาศไว้

  • คำนวณไดเจสต์ ณ เวลาที่สร้างหรือเผยแพร่ ไม่ใช่จากการตอบกลับที่ไม่น่าเชื่อถือแบบเรียลไทม์: สร้างแฮชความสมบูรณ์ในระหว่างขั้นตอนการสร้างหรือเผยแพร่จากไบต์ของเนื้อหาในเครื่องที่ทราบว่าปลอดภัย (หรือคัดลอกไดเจสต์อย่างเป็นทางการที่เผยแพร่โดยผู้เขียนแพ็กเกจ) และฮาร์ดโค้ดลงใน HTML, CSS หรือ JavaScript อย่าดึงข้อมูล URL ของบุคคลที่สามในเบราว์เซอร์ที่รันไทม์เพื่อคำนวณแฮช integrity ของตัวเอง ซึ่งจะเชื่อถือไบต์ใดก็ตามที่เซิร์ฟเวอร์ระยะไกลแสดงผลและเอาชนะ SRI
  • ใช้ sha384- โดยค่าเริ่มต้น: จัดรูปแบบข้อมูลเมตาความสมบูรณ์เป็น <algorithm>-<base64-digest> โดยใช้ sha384- (ค่าเริ่มต้นที่แนะนำ), sha512- หรือ sha256-
  • ปักหมุดทรัพยากรย่อยภายนอกไว้กับ URL ที่ไม่เปลี่ยนรูปและมีการกำหนดเวอร์ชัน: อ้างอิงชิ้นงานของบุคคลที่สามโดยใช้ URL ที่ไม่เปลี่ยนรูป มีการกำหนดเวอร์ชัน หรือระบุเนื้อหา (เช่น https://cdn.example.com/ui@2.4.0/app.min.js) เสมอ แทนที่จะใช้ URL แบบลอยที่เปลี่ยนแปลงได้ (latest/app.min.js) ซึ่งทำให้เว็บไซต์ของคุณเสี่ยงต่อการเปลี่ยนแปลงซัพพลายเชนโดยไม่แจ้งให้ทราบ และทำให้ SRI เสียหายทันทีที่ไฟล์ระยะไกลอัปเดต หากคุณต้องโหลดสคริปต์ของบุคคลที่สามที่เปลี่ยนแปลงได้ซึ่งอัปเดตในตำแหน่ง (เช่น แท็ก Analytics ของผู้ให้บริการ) ให้ละเว้น integrity ใน URL นั้นและจำกัดด้วยนโยบายรักษาความปลอดภัยเนื้อหา (strict-dynamic / Nonce) หรือ iframe Sandboxing แทน
  • แยกไดเจสต์หลายรายการด้วยการเว้นวรรคเมื่อปลายทางอาจแสดงเพย์โหลดที่ถูกต้องหลายรายการ: เบราว์เซอร์ทั้งหมดที่รองรับ SRI จะใช้ sha256-, sha384- และ sha512- หาก URL อาจแสดงผลเพย์โหลดที่ถูกต้องรายการใดรายการหนึ่งจากหลายรายการ (เช่น ระหว่างการทดสอบ A/B, การเปิดตัว CDN แบบแบ่งระยะ หรือบิลด์ที่เจรจาเนื้อหา) ให้ระบุไดเจสต์หลายรายการของอัลกอริทึมเดียวกันโดยคั่นด้วยช่องว่าง (เช่น integrity="sha384-variantA... sha384-variantB...") เบราว์เซอร์จะยอมรับการตอบกลับหากตรงกับไดเจสต์ที่ระบุรายการใดก็ได้ของอัลกอริทึมที่รัดกุมที่สุดที่มีอยู่
  • แสดงทรัพยากรย่อยแบบข้ามต้นทางด้วยส่วนหัว CORS: เบราว์เซอร์จะยืนยันความสมบูรณ์ของการตอบกลับแบบข้ามต้นทางได้ก็ต่อเมื่อ CORS อ่านได้ (Access-Control-Allow-Origin: * สำหรับคำขอที่ไม่ระบุตัวตน หรือต้นทางที่ส่งคำขอที่ตรงกันทุกประการด้วย Access-Control-Allow-Credentials: true สำหรับคำขอที่มีข้อมูลเข้าสู่ระบบ)
# 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. สคริปต์ HTML, สไตล์ชีต และการโหลดล่วงหน้าที่ปลอดภัย

ตั้งค่าส่วนหัวการตอบกลับ HTTP Referrer-Policy: strict-origin-when-cross-origin ระดับเอกสาร (หรือรวม <meta name="referrer" content="strict-origin-when-cross-origin"> ไว้ใน <head>) เป็นค่าเริ่มต้นพื้นฐาน เพื่อให้ทรัพยากรย่อยหรือ url() ของ CSS ที่ละเว้นตัวแก้ไข URL ที่มาต่อคำขอยังคงตัดพารามิเตอร์เส้นทางและพารามิเตอร์การค้นหาที่ละเอียดอ่อนในคำขอแบบข้ามต้นทาง

จากนั้นให้ตั้งค่า integrity, crossorigin และ referrerpolicy ร่วมกันเสมอในองค์ประกอบ <script>, <link rel="stylesheet">, <link rel="preload"> และ <link rel="modulepreload"> ภายนอก ดังนี้

  • integrity="sha384-...": บล็อกการดำเนินการหรือการใช้ทรัพยากรหากไบต์การตอบกลับไม่ตรงกับไดเจสต์ที่คาดไว้ (สำหรับ <link rel="stylesheet"> ที่ถูกบล็อก linkElement.sheet จะยังคงเป็น null และเหตุการณ์ error จะเริ่มทำงาน)
  • crossorigin="anonymous" (ค่าเริ่มต้นที่แนะนำสำหรับชิ้นงาน CDN สาธารณะ) หรือ crossorigin="use-credentials": ต้องระบุเมื่อใดก็ตามที่มีการตั้งค่า integrity ในทรัพยากรแบบข้ามต้นทาง การละเว้น crossorigin จะทำให้เบราว์เซอร์ดึงข้อมูลทรัพยากรในโหมด no-cors (สร้างการตอบกลับแบบทึบแสงซึ่งไม่สามารถยืนยันแฮชได้) และบล็อกทรัพยากร
  • referrerpolicy="no-referrer" (ค่าเริ่มต้นที่แนะนำสำหรับ CDN ของบุคคลที่สาม) หรือ referrerpolicy="strict-origin-when-cross-origin": ลบล้างนโยบายผู้้างอิงเอกสารเพื่อป้องกันไม่ให้ข้อมูลต้นทางหรือเส้นทางรั่วไหลในส่วนหัวของคำขอ Referer ไปยังโฮสต์ของบุคคลที่สาม
<!-- 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>

เมื่อดึงข้อมูลทรัพยากรย่อยภายนอกแบบเป็นโปรแกรมด้วย fetch() ให้ส่งตัวเลือก integrity, mode, credentials และ referrerPolicy ที่ตรงกัน (ไดเจสต์ที่ไม่ตรงกันจะปฏิเสธสัญญาด้วย 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. รักษาความปลอดภัยรูปภาพ CSS, แบบอักษร และสไตล์ชีตที่นำเข้าด้วยตัวแก้ไข url()

ตัวแก้ไขคำขอ url() ของ CSS (cross-origin(), referrer-policy() และ integrity()) จะควบคุมโหมดการดึงข้อมูล ส่วนหัวของ URL ที่มา และการยืนยันความสมบูรณ์ของทรัพยากรย่อยของทรัพยากรที่โหลด CSS ซึ่งรวมถึงกฎ background-image, content, mask-image, border-image-source, filter, @font-face src และ @import โดยตรงจาก CSS ดังนี้

ฟังก์ชันตัวแก้ไข ไวยากรณ์อาร์กิวเมนต์และค่าที่อนุญาต วัตถุประสงค์
cross-origin(<ident>) คีย์เวิร์ดที่ไม่มีเครื่องหมายคำพูด: anonymous (ค่าเริ่มต้นที่แนะนำ) หรือ use-credentials ดึงข้อมูลทรัพยากร CSS โดยใช้ CORS และควบคุมว่าจะส่งข้อมูลเข้าสู่ระบบหรือไม่ ใส่ไว้ข้าง integrity() ในทรัพยากร CSS แบบข้ามต้นทางเสมอ
referrer-policy(<ident>) คีย์เวิร์ดที่ไม่มีเครื่องหมายคำพูด: no-referrer (ค่าเริ่มต้นที่แนะนำสำหรับเนื้อหาของบุคคลที่สาม), strict-origin-when-cross-origin, origin, same-origin, strict-origin, origin-when-cross-origin, no-referrer-when-downgrade หรือ unsafe-url ลบล้างนโยบาย URL ที่มาของเอกสารสำหรับคำขอ CSS ที่เฉพาะเจาะจงนี้
integrity("<string>") สตริงที่ยกมา: "sha384-<base64>" (หรือไดเจสต์ที่คั่นด้วยเว้นวรรค "sha384-... sha384-...") ยืนยันทรัพยากร CSS ที่ดึงมากับไดเจสต์การเข้ารหัสที่คาดไว้ก่อนแสดงผลหรือนำไปใช้

โปรดปฏิบัติตามกฎไวยากรณ์และความปลอดภัยของ CSS ต่อไปนี้เมื่อเขียนตัวแก้ไข url()

  • ใส่เครื่องหมายคำพูดสตริง URL ภายใน url("..."): ตัวแก้ไขคำขอจะใช้ได้ก็ต่อเมื่อ URL ภายใน url(...) เป็น <string> ที่ใส่เครื่องหมายคำพูด (URL ที่ไม่ได้ใส่เครื่องหมายคำพูดจะใช้ตัวแก้ไขไม่ได้)
  • ระบุตัวแก้ไขแต่ละประเภทได้สูงสุด 1 ครั้งต่อ url(): คุณสามารถรวม cross-origin(), referrer-policy() และ integrity() ตามลำดับใดก็ได้ภายใน url("..." ...) แต่การใช้ตัวแก้ไขประเภทเดียวกันซ้ำ 2 ครั้ง (เช่น ตัวแก้ไข integrity() 2 ตัว) จะไม่ถูกต้อง หากต้องการระบุแฮชหลายรายการ ให้เว้นวรรคระหว่างแฮชภายในสตริง integrity("sha384-... sha384-...") เดียว
  • จับคู่ integrity() แบบข้ามต้นทางกับ cross-origin(anonymous) เสมอ: แม้ว่า @font-face src และ mask-image จะดึงข้อมูลในโหมด cors แบบไม่ระบุตัวตนโดยค่าเริ่มต้นอยู่แล้ว แต่ background-image, content, border-image-source และ @import จะใช้โหมด no-cors โดยค่าเริ่มต้น (สร้างการตอบกลับแบบทึบแสงซึ่งไม่สามารถยืนยันแฮชได้) และ filter SVG ภายนอกจะใช้โหมด same-origin โดยค่าเริ่มต้น (บล็อก URL แบบข้ามต้นทางโดยสิ้นเชิง) การรวม cross-origin(anonymous) (หรือ cross-origin(use-credentials)) จะช่วยให้มั่นใจได้ว่าการยืนยัน CORS จะสอดคล้องกันในพร็อพเพอร์ตี้ CSS ทุกรายการ
  • วางตัวแก้ไขไว้ใน url("...") ในกฎ @import และ @font-face: เก็บ cross-origin(), referrer-policy() และ integrity() ไว้ใน url("..." ...) ขณะที่เก็บข้อบ่งชี้แบบอักษร เช่น format("woff2") และ tech(...) ไว้นอก 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")
  );
}

กลยุทธ์สำรอง

หากเป้าหมาย Baseline ยังไม่รองรับตัวปรับแต่งคำขอ CSS url() (cross-origin(), referrer-policy() หรือ integrity()) ให้รวมนโยบายระดับเอกสารเข้ากับการประกาศ CSS แบบเรียงซ้อนเพื่อให้ทรัพยากรย่อยยังคงทำงานได้และได้รับการปกป้อง ดังนี้

  1. ใช้ HTML <link rel="stylesheet"> แทน CSS @import เมื่อต้องใช้ SRI ข้ามเบราว์เซอร์: เนื่องจาก subresource-integrity และ referrer-policy ใน <link rel="stylesheet"> มีให้บริการอย่างแพร่หลายในทุกเบราว์เซอร์ ให้โหลดสไตล์ชีตภายนอกด้วย <link rel="stylesheet" href="..." integrity="sha384-..." crossorigin="anonymous" referrerpolicy="no-referrer"> แทน @import เมื่อใดก็ตามที่ต้องบังคับใช้การยืนยันความสมบูรณ์ในทุกเบราว์เซอร์
  2. ใช้ส่วนหัว Referrer-Policy ระดับเอกสารหรือ <meta name="referrer"> จากขั้นตอนที่ 2: ในเบราว์เซอร์ที่ไม่รองรับ referrer-policy() ภายใน CSS url() ส่วนหัว Referrer-Policy: strict-origin-when-cross-origin ระดับเอกสารหรือองค์ประกอบ <meta name="referrer" content="strict-origin-when-cross-origin"> จะตัดพารามิเตอร์เส้นทางและพารามิเตอร์การค้นหาที่ละเอียดอ่อนในคำขอ CSS แบบข้ามต้นทางโดยอัตโนมัติ
  3. ประกาศ CSS แยกต่างหากแบบเรียงซ้อนเพื่อการเพิ่มประสิทธิภาพแบบต่อเนื่อง: เบราว์เซอร์ที่ไม่รู้จักตัวปรับแต่งภายใน url("..." ...) จะทิ้งประกาศทั้งหมดเมื่อแยกวิเคราะห์ CSS ประกาศ url("...") ธรรมดาก่อน ตามด้วยการประกาศ cross-origin() และ referrer-policy() (สำหรับเบราว์เซอร์ที่รองรับตัวแก้ไข 2 ตัวนั้นแต่ไม่รองรับ integrity()) ตามด้วยการประกาศแบบเต็มซึ่งรวมถึง integrity("sha384-...") ดังนี้

    • อย่าใส่ url() ที่ไม่ได้รับการยืนยันไว้ในรายการ @font-face src ที่คั่นด้วยคอมมาเดียวกัน: การเขียน src: url("font.woff2" cross-origin(anonymous) integrity("sha384-...")), url("font.woff2"); ในข้อบ่งชี้ src รายการเดียวจะข้ามความสมบูรณ์ของทรัพยากรย่อยที่รันไทม์ หาก CDN แสดงแบบอักษรที่ถูกดัดแปลงซึ่งไม่ผ่านการตรวจสอบ integrity() เบราว์เซอร์จะถือว่าโหลดไม่สำเร็จและจะกลับไปใช้ url("font.woff2") ที่คั่นด้วยคอมมาที่ 2 โดยอัตโนมัติ การใช้การประกาศ src: แยกต่างหากช่วยให้มั่นใจได้ว่าเบราว์เซอร์ที่รองรับจะเขียนทับการประกาศที่ยังไม่ได้รับการยืนยัน ณ เวลาที่แยกวิเคราะห์ CSS และบล็อกแบบอักษรที่ถูกดัดแปลง ณ รันไทม์
    @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. ตรวจหาฟีเจอร์การรองรับตัวแก้ไข url() ของ CSS ใน JavaScript ด้วย 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 };
}