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

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