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

Krzysztof Kotowicz
Krzysztof Kotowicz

การรองรับเบราว์เซอร์

  • Chrome: 83
  • Edge: 83
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

แหล่งที่มา

สคริปต์ข้ามเว็บไซต์ที่อิงตาม DOM (DOM XSS) เกิดขึ้นเมื่อข้อมูลจากแหล่งที่มาที่ผู้ใช้ควบคุม (เช่น ชื่อผู้ใช้หรือ URL เปลี่ยนเส้นทางที่มาจาก URL เศษ) ไปถึง sink ซึ่งเป็นฟังก์ชันอย่าง eval() หรือตัวตั้งค่าพร็อพเพอร์ตี้อย่าง .innerHTML ที่สามารถเรียกใช้โค้ด JavaScript ที่กำหนดเอง

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

ข้อมูลเบื้องต้น

DOM XSS เป็นช่องโหว่ด้านความปลอดภัยของเว็บที่พบบ่อยและอันตรายที่สุดอย่างหนึ่งมาเป็นเวลาหลายปี

สคริปต์ข้ามเว็บไซต์มี 2 ประเภท ช่องโหว่ XSS บางรายการเกิดจากโค้ดฝั่งเซิร์ฟเวอร์ที่สร้างโค้ด HTML ขึ้นอย่างไม่ปลอดภัยซึ่งประกอบกันเป็นเว็บไซต์ ส่วนปัญหาอื่นๆ มีสาเหตุหลักมาจากไคลเอ็นต์ ซึ่งโค้ด JavaScript เรียกใช้ฟังก์ชันที่เป็นอันตรายกับเนื้อหาที่ผู้ใช้ควบคุม

หากต้องการป้องกัน XSS ฝั่งเซิร์ฟเวอร์ อย่าสร้าง HTML โดยการต่อสตริง ใช้ไลบรารีเทมเพลตที่แปลงอักขระอัตโนมัติตามบริบทอย่างปลอดภัยแทน พร้อมกับนโยบายความปลอดภัยของเนื้อหาที่อิงตาม Nonce เพื่อลดข้อบกพร่องเพิ่มเติม

ตอนนี้เบราว์เซอร์ยังช่วยป้องกัน XSS ที่ใช้ DOM ฝั่งไคลเอ็นต์ได้ด้วยโดยใช้ประเภทที่เชื่อถือได้

ข้อมูลเบื้องต้นเกี่ยวกับ API

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

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

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

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

ควรทำ
anElement.innerHTML = aTrustedHTML;
  
เมื่อเปิดใช้ประเภทที่เชื่อถือแล้ว เบราว์เซอร์จะยอมรับออบเจ็กต์ TrustedHTML สำหรับซิงค์ที่คาดหวังข้อมูลโค้ด HTML นอกจากนี้ยังมีออบเจ็กต์ TrustedScript และ TrustedScriptURL สำหรับอ่างล้างมืออื่นๆ ที่ละเอียดอ่อน

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

วิธีใช้ประเภทที่เชื่อถือได้

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

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

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

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

แก้ไขการละเมิด

การแก้ไขการละเมิดประเภทที่เชื่อถือได้ทำได้ 2 วิธี คุณสามารถนําโค้ดที่ทำให้เกิดข้อผิดพลาดออก ใช้ไลบรารี สร้างนโยบายประเภทที่เชื่อถือได้ หรือสร้างนโยบายเริ่มต้นเป็นทางเลือกสุดท้าย

เขียนโค้ดที่ไม่เหมาะสมใหม่

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

ควรทำ
el.textContent = '';
const img = document.createElement('img');
img.src = 'xyz.jpg';
el.appendChild(img);
ไม่ควรทำ
el.innerHTML = '<img src=xyz.jpg>';

ใช้ไลบรารี

ไลบรารีบางรายการสร้างประเภทที่เชื่อถือได้ซึ่งคุณสามารถส่งไปยังฟังก์ชันที่รับข้อมูลได้อยู่แล้ว เช่น คุณสามารถใช้ DOMPurify เพื่อล้างข้อมูลโค้ด HTML โดยนำเพย์โหลด XSS ออก

import DOMPurify from 'dompurify';
el.innerHTML = DOMPurify.sanitize(html, {RETURN_TRUSTED_TYPE: true});

DOMPurify รองรับประเภทที่เชื่อถือได้ และแสดงผล HTML ที่ผ่านการกรองแล้วซึ่งรวมอยู่ในออบเจ็กต์ TrustedHTML เพื่อให้เบราว์เซอร์ไม่สร้างการละเมิด

สร้างนโยบายประเภทที่เชื่อถือได้

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

ก่อนอื่นให้สร้างนโยบาย นโยบายคือโรงงานสําหรับ 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 ในทุกที่ที่มีการใช้สตริงในซิงค์ที่ยอมรับเฉพาะประเภทที่เชื่อถือได้

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

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

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

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

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