ป้องกันช่องโหว่ในการเขียนสคริปต์ข้ามเว็บไซต์ตาม DOM ด้วยประเภทที่เชื่อถือได้

Krzysztof Kotowicz
Krzysztof Kotowicz

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: behind a flag.
  • Safari: 26.

Source

การเขียนสคริปต์ข้ามเว็บไซต์ที่อิงตาม 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 ที่มีความเสี่ยงต่อไปนี้ คุณอาจคุ้นเคยกับฟีเจอร์บางอย่างอยู่แล้ว เนื่องจากผู้ให้บริการเบราว์เซอร์และเฟรมเวิร์กของเว็บได้แนะนำให้คุณหลีกเลี่ยงการใช้ฟีเจอร์เหล่านี้ด้วยเหตุผลด้านความปลอดภัย

Trusted Types กำหนดให้คุณต้องประมวลผลข้อมูลก่อนส่งไปยังฟังก์ชัน Sink เหล่านี้ การใช้เฉพาะสตริงจะล้มเหลว เนื่องจากเบราว์เซอร์ไม่ทราบ ว่าข้อมูลเชื่อถือได้หรือไม่

ไม่ควรทำ
anElement.innerHTML  = location.href;
เมื่อเปิดใช้ Trusted Types เบราว์เซอร์จะแสดง TypeError และป้องกัน การใช้ Sink ของ DOM XSS กับสตริง

หากต้องการระบุว่ามีการประมวลผลข้อมูลอย่างปลอดภัย ให้สร้างออบเจ็กต์พิเศษ ซึ่งก็คือ Trusted Type

ควรทำ
anElement.innerHTML = aTrustedHTML;
  
เมื่อเปิดใช้ Trusted Types เบราว์เซอร์จะยอมรับออบเจ็กต์ 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, '&lt;')
  });
}

โค้ดนี้สร้างนโยบายชื่อ myEscapePolicy ซึ่งสร้างออบเจ็กต์ TrustedHTML ได้โดยใช้ฟังก์ชัน createHTML() กฎที่กำหนดจะหลีกเลี่ยงอักขระ < ใน HTML เพื่อป้องกันการสร้างองค์ประกอบ HTML ใหม่

ใช้นโยบายดังนี้

const escaped = escapeHTMLPolicy.createHTML('<img src=x ');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;img src=x '

ใช้นโยบายเริ่มต้น

ในบางครั้ง คุณอาจเปลี่ยนโค้ดที่เป็นปัญหาไม่ได้ เช่น หากโหลดไลบรารีของบุคคลที่สามจาก 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 คือโค้ดในนโยบายข้อใดข้อหนึ่ง และคุณสามารถเพิ่มความปลอดภัยได้มากขึ้นด้วยการจำกัดการสร้างนโยบาย

อ่านเพิ่มเติม