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

Krzysztof Kotowicz
Krzysztof Kotowicz

การสนับสนุนเบราว์เซอร์

  • 83
  • 83
  • x
  • x

แหล่งที่มา

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

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

ที่มา

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

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

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

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

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

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

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

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

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

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

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

วิธีใช้ Trusted Types

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

คุณจะใช้เครื่องมือรวบรวมข้อมูลรายงาน เช่น go-csp-collector แบบโอเพนซอร์ส หรือใช้เครื่องมือที่เทียบเท่าในเชิงพาณิชย์ก็ได้ คุณยังแก้ไขข้อบกพร่องของการละเมิดในเบราว์เซอร์ได้ด้วย โดยทำดังนี้

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

จากนี้ไป ทุกครั้งที่ 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 = '';

ใช้ไลบรารี

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

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

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

สร้างนโยบาย Trusted Type

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

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

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

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

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

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