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

Krzysztof Kotowicz
Krzysztof Kotowicz

Browser Support

  • Chrome: 83.
  • Edge: 83.
  • Firefox: 148.
  • Safari: 26.

Source

Cross-site Scripting (DOM XSS) ที่อิงตาม DOM เกิดขึ้นเมื่อข้อมูลจากa _แหล่งที่มา_ ซึ่งผู้ใช้ควบคุมได้ (เช่น ชื่อผู้ใช้หรือ URL เปลี่ยนเส้นทางที่นำมาจากส่วนย่อย URL ) ไปถึง _Sink_ ซึ่งเป็นฟังก์ชันอย่าง 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 เพื่อลดข้อบกพร่องเพิ่มเติม

ตอนนี้เบราว์เซอร์ยังช่วยป้องกัน DOM 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 ออบเจ็กต์สำหรับ Sink ที่มีความละเอียดอ่อนอื่นๆ

Trusted Types ช่วยลดพื้นผิวการโจมตี DOM XSS ของแอปพลิเคชันได้อย่างมาก โดยจะลดความซับซ้อนในการตรวจสอ��ความปลอดภัย และช่วยให้คุณบังคับใช้การตรวจสอบความปลอดภัยตามประเภทที่ทำเมื่อคอมไพล์ Lint หรือรวมโค้ดที่รันไทม์ในเบราว์เซอร์

วิธีใช้ Trusted Types

เตรียมพร้อมสำหรับรายงานการละเมิดนโยบายรักษาความปลอดภัยเนื้อหา

คุณสามารถติดตั้งใช้งานตัวรวบรวมรายงาน เช่น 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 Types

นับจากนี้ไป ทุกครั้งที่ Trusted Types ตรวจพบการละเมิด เบราว์เซอร์จะส่งรายงานไปยัง 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 Type ทำได้ 2 วิธี คุณสามารถ นำโค้ดที่ทำให้เกิดปัญหาออก, ใช้ไลบรารี, สร้างนโยบาย Trusted Type หรือสร้างนโยบายเริ่มต้นเป็นทางเลือกสุดท้าย

เขียนโค้ดที่ทำให้เกิดปัญหาใหม่

โค้ดที่ไม่เป็นไปตามข้อกำหนดอาจไม่จำเป็นอีกต่อไป หรือสามารถเขียนใหม่ได้โดยไม่ต้องใช้ฟังก์ชันที่ทำให้เกิดการละเมิด ดังนี้

ควรทำ
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 Type

บางครั้งคุณไม่สามารถนำโค้ดที่ทำให้เกิดการละเมิดออก และไม่ม���ไลบรารีที่จะล้างข้อมูลค่าและสร้าง Trusted Type ให้คุณ ในกรณีดังกล่าว คุณสามารถสร้างออบเจ็กต์ 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 onerror=alert(1)>');
console.log(escaped instanceof TrustedHTML);  // true
el.innerHTML = escaped;  // '&lt;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 Type

เปลี่ยนไปบังคับใช้นโยบายรักษาความปลอดภัยเนื้อหา

เมื่อแอปพลิเคชันไม่สร้างการละเมิดอีกต่อไป คุณจะเริ่มบังคับใช้ Trusted Types ได้

Content-Security-Policy: require-trusted-types-for 'script'; report-uri //my-csp-endpoint.example

ตอนนี้ไม่ว่าเว็บแอปพลิเคชันของคุณจะซับซ้อนเพียงใด สิ่งเดียวที่อาจทำให้เกิดช่องโหว่ DOM XSS คือโค้ดในนโยบายใดนโยบายหนึ่ง และคุณสามารถล็อกโค้ดนั้นได้มากยิ่งขึ้นโดยการจำกัดการสร้างนโยบาย

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