เมื่อโหลดสคริปต์ สไตล์ชีต แบบอักษร หรือรูปภาพจาก 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-facesrcและmask-imageจะดึงข้อมูลในโหมดcorsแบบไม่ระบุตัวตนโดยค่าเริ่มต้นอยู่แล้ว แต่background-image,content,border-image-sourceและ@importจะใช้โหมดno-corsโดยค่าเริ่มต้น (สร้างการตอบกลับแบบทึบแสงซึ่งไม่สามารถยืนยันแฮชได้) และfilterSVG ภายนอกจะใช้โหมด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 แบบเรียงซ้อนเพื่อให้ทรัพยากรย่อยยังคงทำงานได้และได้รับการปกป้อง ดังนี้
- ใช้ 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เมื่อใดก็ตามที่ต้องบังคับใช้การยืนยันความสมบูรณ์ในทุกเบราว์เซอร์ - ใช้ส่วนหัว
Referrer-Policyระดับเอกสารหรือ<meta name="referrer">จากขั้นตอนที่ 2: ในเบราว์เซอร์ที่ไม่รองรับreferrer-policy()ภายใน CSSurl()ส่วนหัวReferrer-Policy: strict-origin-when-cross-originระดับเอกสารหรือองค์ประกอบ<meta name="referrer" content="strict-origin-when-cross-origin">จะตัดพารามิเตอร์เส้นทางและพารามิเตอร์การค้นหาที่ละเอียดอ่อนในคำขอ CSS แบบข้ามต้นทางโดยอัตโนมัติ ประกาศ CSS แยกต่างหากแบบเรียงซ้อนเพื่อการเพิ่มประสิทธิภาพแบบต่อเนื่อง: เบราว์เซอร์ที่ไม่รู้จักตัวปรับแต่งภายใน
url("..." ...)จะทิ้งประกาศทั้งหมดเมื่อแยกวิเคราะห์ CSS ประกาศurl("...")ธรรมดาก่อน ตามด้วยการประกาศcross-origin()และreferrer-policy()(สำหรับเบราว์เซอร์ที่รองรับตัวแก้ไข 2 ตัวนั้นแต่ไม่รองรับintegrity()) ตามด้วยการประกาศแบบเต็มซึ่งรวมถึงintegrity("sha384-...")ดังนี้- อย่าใส่
url()ที่ไม่ได้รับการยืนยันไว้ในรายการ@font-facesrcที่คั่นด้วยคอมมาเดียวกัน: การเขียน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") ); }- อย่าใส่
ตรวจหาฟีเจอร์การรองรับตัวแก้ไข
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 };
}