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

Krzysztof Kotowicz
Krzysztof Kotowicz

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

  • 83
  • 83
  • x
  • x

แหล่งที่มา

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

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

ที่มา

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

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

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

ปัจจุบันเบราว์เซอร์ยังช่วยป้องกัน XSS ที่อิงตาม DOM ฝั่งไคลเอ็นต์ได้โดยใช้ Trusted Types

แนะนำ API

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

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

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

หากต้องการบ่งบอกว่าข้อมูลได้รับการประมวลผลอย่างปลอดภัยแล้ว ให้สร้างออบเจ็กต์พิเศษ - Trusted Type

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

ประเภทที่เชื่อถือได้จะลดหน้าการโจมตี 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

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

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

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

ก่อนอื่น ให้สร้างนโยบาย นโยบายคือโรงงานสำหรับประเภท Trusted ซึ่งบังคับใช้กฎความปลอดภัยบางอย่างกับอินพุตต่อไปนี้

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

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

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

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

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

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