การเขียนสคริปต์ข้ามเว็บไซต์ที่อิงตาม DOM (DOM XSS) เกิดขึ้นเมื่อข้อมูลจากแหล่งที่มาที่ผู้ใช้ควบคุม (เช่น ชื่อผู้ใช้หรือ URL เปลี่ยนเส้นทางที่นำมาจากส่วนย่อย URL) ไปถึงปลายทาง ซึ่งเป็นฟังก์ชัน เช่น eval()
หรือตัวตั้งค่าพร็อพเพอร์ตี้ เช่น .innerHTML
ที่สามารถเรียกใช้โค้ด JavaScript ที่กำหนดเองได้
DOM XSS เป็นหนึ่งในช่องโหว่ด้านความปลอดภัยของเว็บที่พบบ่อยที่สุด และทีมพัฒนาอาจเผลอใส่ช่องโหว่นี้ลงในแอป Trusted Types ช่วยให้คุณมี เครื่องมือในการเขียน ตรวจสอบความปลอดภัย และทำให้แอปพลิเคชันปราศจากช่องโหว่ DOM XSS โดยทำให้ฟังก์ชัน Web API ที่เป็นอันตรายมีความปลอดภัยโดยค่าเริ่มต้น Trusted Types พร้อมให้บริการเป็น polyfill สำหรับเบราว์เซอร์ที่ยังไม่รองรับ
ฉากหลัง
DOM XSS เป็นช่องโหว่ด้านความปลอดภัยบนเว็บที่พบได้บ่อยและอันตรายที่สุดมาเป็นเวลาหลายปี
Cross-Site Scripting มี 2 ประเภท ช่องโหว่ XSS บางอย่างเกิดจากโค้ดฝั่งเซิร์ฟเวอร์ที่สร้างโค้ด HTML ที่ประกอบเป็นเว็บไซต์อย่างไม่ปลอดภัย ส่วนสาเหตุอื่นๆ เกิดขึ้นที่ไคลเอ็นต์ ซึ่งโค้ด JavaScript เรียกฟังก์ชันที่เป็นอันตรายพร้อมเนื้อหาที่ผู้ใช้ควบคุม
หากต้องการป้องกัน XSS ฝั่งเซิร์ฟเวอร์ อย่าสร้าง HTML โดยการต่อสตริง ให้ใช้ไลบรารีการสร้างเทมเพลตที่มีการหลีกเลี่ยงโดยอัตโนมัติตามบริบทที่ปลอดภัยแทน พร้อมกับนโยบายความปลอดภัยของเนื้อหาตาม Nonce เพื่อลดข้อบกพร่องเพิ่มเติม
ตอนนี้เบราว์เซอร์ยังช่วยป้องกัน XSS ที่อิงตาม DOM ฝั่งไคลเอ็นต์ได้ด้วยการใช้ Trusted Types
ข้อมูลเบื้องต้นเกี่ยวกับ API
Trusted Types ทำงานโดยการล็อกฟังก์ชัน Sink ที่มีความเสี่ยงต่อไปนี้ คุณอาจคุ้นเคยกับฟีเจอร์บางอย่างอยู่แล้ว เนื่องจากผู้ให้บริการเบราว์เซอร์และเฟรมเวิร์กของเว็บได้แนะนำให้คุณหลีกเลี่ยงการใช้ฟีเจอร์เหล่านี้ด้วยเหตุผลด้านความปลอดภัย
- การจัดการสคริปต์:
<script src>
และการตั้งค่าเนื้อหาข้อความของ<script>
องค์ประกอบ - การสร้าง HTML จากสตริง
- การเรียกใช้เนื้อหาปลั๊กอิน
- การคอมไพล์โค้ด JavaScript รันไทม์
eval
setTimeout
setInterval
new Function()
Trusted Types กำหนดให้คุณต้องประมวลผลข้อมูลก่อนส่งไปยังฟังก์ชัน Sink เหล่านี้ การใช้เฉพาะสตริงจะล้มเหลว เนื่องจากเบราว์เซอร์ไม่ทราบ ว่าข้อมูลเชื่อถือได้หรือไม่
anElement.innerHTML = location.href;
หากต้องการระบุว่ามีการประมวลผลข้อมูลอย่างปลอดภัย ให้สร้างออบเจ็กต์พิเศษ ซึ่งก็คือ Trusted Type
anElement.innerHTML = aTrustedHTML;
TrustedHTML
สำหรับ Sink ที่คาดหวังตัวอย่าง HTML นอกจากนี้ ยังมีออบเจ็กต์ TrustedScript
และ TrustedScriptURL
สำหรับ
ซิงก์ที่มีความละเอียดอ่อนอื่นๆ ด้วย
Trusted Types ช่วยลดพื้นผิวการโจมตี DOM XSS ของแอปพลิเคชันได้อย่างมาก ซึ่งจะช่วยลดความซับซ้อนในการตรวจสอบความปลอดภัย และช่วยให้คุณบังคับใช้ การตรวจสอบความปลอดภัยตามประเภทที่ทำเมื่อคอมไพล์ Lint หรือจัดกลุ่มโค้ด ในรันไทม์ในเบราว์เซอร์ได้
วิธีใช้ประเภทที่เชื่อถือได้
เตรียมพร้อมสำหรับรายงานการละเมิดนโยบายความปลอดภัยของเนื้อหา
คุณสามารถติดตั้งใช้งานเครื่องมือรวบรวมรายงาน เช่น reporting-api-processor หรือ go-csp-collector แบบโอเพนซอร์ส หรือใช้เครื่องมือที่เทียบเท่าในเชิงพาณิชย์ นอกจากนี้ คุณยังเพิ่มการบันทึกที่กำหนดเองและแก้ไขข้อบกพร่องของการละเมิดในเบราว์เซอร์ได้โดยใช้ ReportingObserver ดังนี้
const observer = new ReportingObserver((reports, observer) => {
for (const report of reports) {
if (report.type !== 'csp-violation' ||
report.body.effectiveDirective !== 'require-trusted-types-for') {
continue;
}
const violation = report.body;
console.log('Trusted Types Violation:', violation);
// ... (rest of your logging and reporting logic)
}
}, { buffered: true });
observer.observe();
หรือโดยการเพิ่ม Listener เหตุการณ์
document.addEventListener('securitypolicyviolation',
console.error.bind(console));
เพิ่มส่วนหัว CSP ที่รายงานเท่านั้น
เพิ่มส่วนหัวการตอบกลับ HTTP ต่อไปนี้ลงในเอกสารที่ต้องการย้ายข้อมูลไปยัง Trusted Types
Content-Security-Policy-Report-Only: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
ตอนนี้ระบบจะรายงานการละเมิดทั้งหมดไปยัง //my-csp-endpoint.example
แต่เว็บไซต์จะยังคงทำงานต่อไป
ส่วนถัดไปจะอธิบายวิธีการทำงานของ
//my-csp-endpoint.example
ระบุการละเมิด Trusted Type
นับจากนี้ไป ทุกครั้งที่ประเภทที่เชื่อถือได้ตรวจพบการละเมิด เบราว์เซอร์จะส่งรายงานไปยัง report-uri
ที่กำหนดค่าไว้ เช่น เมื่อแอปพลิเคชัน
ส่งสตริงไปยัง innerHTML
เบราว์เซอร์จะส่งรายงานต่อไปนี้
{
"csp-report": {
"document-uri": "https://my.url.example",
"violated-directive": "require-trusted-types-for",
"disposition": "report",
"blocked-uri": "trusted-types-sink",
"line-number": 39,
"column-number": 12,
"source-file": "https://my.url.example/script.js",
"status-code": 0,
"script-sample": "Element innerHTML <img src=x"
}
}
ข้อความนี้ระบุว่าใน https://my.url.example/script.js
ที่บรรทัด 39 มีการเรียก innerHTML
ด้วยสตริงที่ขึ้นต้นด้วย <img src=x
ข้อมูลนี้จะช่วยให้คุณ
จำกัดส่วนของโค้ดที่อาจทำให้เกิด DOM XSS และต้องเปลี่ยนแปลง
แก้ไขการละเมิด
การแก้ไขการละเมิด Trusted Types ทำได้ 2 วิธี คุณสามารถนำโค้ดที่เป็นปัญหาออก ใช้ไลบรารี สร้างนโยบายประเภทที่เชื่อถือได้ หรือสร้างนโยบายเริ่มต้นเป็นทางเลือกสุดท้าย
เขียนโค้ดที่เป็นปัญหาใหม่
โค้ดที่ไม่เป็นไปตามข้อกำหนดอาจไม่จำเป็นอีกต่อไป หรืออาจเขียนใหม่ได้โดยไม่ต้องใช้ฟังก์ชันที่ทำให้เกิดการละเมิด
el.textContent = ''; const img = document.createElement('img'); img.src = 'xyz.jpg'; el.appendChild(img);
el.innerHTML = '<img src=xyz.jpg>';
ใช้ไลบรารี
ไลบรารีบางรายการสร้าง Trusted Types ที่คุณส่งไปยังฟังก์ชัน Sink ได้อยู่แล้ว เช่น คุณสามารถใช้ DOMPurify เพื่อล้างข้อมูลโค้ด HTML โดยนำเพย์โหลด XSS ออก
import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});
DOMPurify รองรับ Trusted Types
และส่งกลับ HTML ที่ผ่านการล้างข้อมูลซึ่งห่อหุ้มไว้ในออบเจ็กต์ TrustedHTML
เพื่อให้เบราว์เซอร์
ไม่สร้างการละเมิด
สร้างนโยบาย Trusted Types
ในบางครั้ง คุณอาจนำโค้ดที่ทำให้เกิดการละเมิดออกไม่ได้ และไม่มีไลบรารีที่จะล้างค่าและสร้าง Trusted Type ให้คุณ ในกรณีดังกล่าว คุณสามารถสร้างออบเจ็กต์ประเภทที่เชื่อถือได้ด้วยตนเอง
ก่อนอื่นให้สร้างนโยบาย นโยบายคือโรงงานสำหรับ Trusted Types ที่บังคับใช้กฎความปลอดภัยบางอย่างกับอินพุตของตน
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
const escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
createHTML: string => string.replace(/\</g, '<')
});
}
โค้ดนี้สร้างนโยบายชื่อ myEscapePolicy
ซึ่งสร้างออบเจ็กต์ TrustedHTML
ได้โดยใช้ฟังก์ชัน createHTML()
กฎที่กำหนดจะหลีกเลี่ยงอักขระ <
ใน HTML เพื่อป้องกันการสร้างองค์ประกอบ HTML ใหม่
ใช้นโยบายดังนี้
const escaped = escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML); // true
el.innerHTML = escaped; // '<img src=x onerror=alert(1)>'
ใช้นโยบายเริ่มต้น
ในบางครั้ง คุณอาจเปลี่ยนโค้ดที่เป็นปัญหาไม่ได้ เช่น หากโหลดไลบรารีของบุคคลที่สามจาก CDN ในกรณีนี้ ให้ใช้นโยบายเริ่มต้นดังนี้
if (window.trustedTypes && trustedTypes.createPolicy) { // Feature testing
trustedTypes.createPolicy('default', {
createHTML: (string, sink) => DOMPurify.sanitize(string, {RETURN_TRUSTED_TYPE: true})
});
}
ระบบจะใช้นโยบายที่ชื่อ default
ทุกที่ที่มีการใช้สตริงใน Sink ที่ยอมรับเฉพาะประเภทที่เชื่อถือได้
เปลี่ยนไปใช้การบังคับใช้นโยบายรักษาความปลอดภัยเนื้อหา
เมื่อแอปพลิเคชันไม่ละเมิดนโยบายอีกต่อไป คุณจะเริ่มบังคับใช้ Trusted Types ได้โดยทำดังนี้
Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example
ตอนนี้ไม่ว่าเว็บแอปพลิเคชันจะซับซ้อนเพียงใด สิ่งเดียวที่อาจทำให้เกิดช่องโหว่ DOM XSS คือโค้ดในนโยบายข้อใดข้อหนึ่ง และคุณสามารถเพิ่มความปลอดภัยได้มากขึ้นด้วยการจำกัดการสร้างนโยบาย