การควบคุม DOM อย่างปลอดภัยด้วย Sanitizer API

Jack J
Jack J

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

ข้อเสนอ Sanitizer API ใหม่มีเป้าหมายเพื่อสร้างตัวประมวลผลที่มีประสิทธิภาพสำหรับสตริงที่กำหนดเองเพื่อแทรกลงในหน้าเว็บได้อย่างปลอดภัย

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

หลีกเลี่ยงข้อมูลจากผู้ใช้

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

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

หากคุณหลีกเลี่ยงสัญลักษณ์พิเศษ HTML ในสตริงอินพุตหรือขยายโดยใช้ .textContent ระบบจะไม่ดำเนินการ alert(0) อย่างไรก็ตาม เนื่องจาก <em> ที่ผู้ใช้เพิ่มจะขยายเป็นสตริงตามที่เป็นอยู่ วิธีนี้จึงใช้ไม่ได้หากต้องการคงการตกแต่งข้อความไว้ใน HTML

สิ่งที่ดีที่สุดที่ควรทำในที่นี้คือไม่ใช่ การหลีกเลี่ยง แต่เป็นการ ล้างข้อมูล

ล้างข้อมูลจากผู้ใช้

การหลีกเลี่ยงหมายถึงการแทนที่อักขระ HTML พิเศษด้วย เอนทิตี HTML

การล้างข้อมูลหมายถึงการนำส่วนที่เป็นอันตรายต่อความหมาย (เช่น การดำเนินการสคริปต์) ออกจากสตริง HTML

ตัวอย่าง

ในตัวอย่างก่อนหน้า <img onerror> ทำให้มีการดำเนินการตัวจัดการข้อผิดพลาด แต่หากนำตัวแฮนเดิล onerror ออก ก็จะขยายใน DOM ได้อย่างปลอดภัยโดยคง <em> ไว้

// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`

หากต้องการล้างข้อมูลอย่างถูกต้อง คุณต้องแยกวิเคราะห์สตริงอินพุตเป็น HTML ละเว้นแท็กและแอตทริบิวต์ที่ถือว่าเป็นอันตราย และเก็บแท็กและแอตทริบิวต์ที่ไม่เป็นอันตรายไว้

ข้อกำหนด Sanitizer API ที่เสนอ มีเป้าหมายเพื่อมอบการประมวลผลดังกล่าวเป็น API มาตรฐานสำหรับเบราว์เซอร์

Sanitizer API

Sanitizer API ใช้ในลักษณะต่อไปนี้

const $div = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.setHTML(user_input, { sanitizer: new Sanitizer() }) // <div><em>hello world</em><img src=""></div>

อย่างไรก็ตาม { sanitizer: new Sanitizer() } เป็นอาร์กิวเมนต์เริ่มต้น

$div.setHTML(user_input) // <div><em>hello world</em><img src=""></div>

ควรทราบว่า setHTML() กำหนดไว้ใน Element เนื่องจากเป็นเมธอดของ Element บริบทที่จะแยกวิเคราะห์จึงอธิบายได้ในตัว (<div> ในกรณีนี้) ระบบจะแยกวิเคราะห์ภายใน 1 ครั้ง และขยายผลลัพธ์ลงใน DOM โดยตรง

หากต้องการรับผลลัพธ์ของการล้างข้อมูลเป็นสตริง คุณสามารถใช้ .innerHTML จากผลลัพธ์ setHTML()

const $div = document.createElement('div')
$div.setHTML(user_input)
$div.innerHTML // <em>hello world</em><img src="">

ปรับแต่งด้วยการกำหนดค่า

Sanitizer API ได้รับการกำหนดค่าเริ่มต้นให้ลบสตริงที่จะทริกเกอร์การดำเนินการสคริปต์ อย่างไรก็ตาม คุณยังเพิ่มการปรับแต่งของคุณเองลงในกระบวนการล้างข้อมูลด้วยออบเจ็กต์การกำหนดค่าได้ด้วย

const config = {
  allowElements: [],
  blockElements: [],
  dropElements: [],
  allowAttributes: {},
  dropAttributes: {},
  allowCustomElements: true,
  allowComments: true
};
// sanitized result is customized by configuration
new Sanitizer(config)

ตัวเลือกต่อไปนี้จะระบุวิธีที่ผลลัพธ์การล้างข้อมูลควรจัดการกับองค์ประกอบที่ระบุ

allowElements: ชื่อขององค์ประกอบที่ตัวล้างข้อมูลควรเก็บไว้

blockElements: ชื่อขององค์ประกอบที่ตัวล้างข้อมูลควรนำออก โดยเก็บองค์ประกอบย่อยไว้

dropElements: ชื่อขององค์ประกอบที่ตัวล้างข้อมูลควรนำออกพร้อมกับองค์ประกอบย่อย

const str = `hello <b><i>world</i></b>`

$div.setHTML(str)
// <div>hello <b><i>world</i></b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: [ "b" ]}) })
// <div>hello <b>world</b></div>

$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ "b" ]}) })
// <div>hello <i>world</i></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowElements: []}) })
// <div>hello world</div>

นอกจากนี้ คุณยังควบคุมได้ว่าตัวล้างข้อมูลจะอนุญาตหรือปฏิเสธแอตทริบิวต์ที่ระบุด้วยตัวเลือกต่อไปนี้

  • allowAttributes
  • dropAttributes

allowAttributes และ dropAttributes พร็อพเพอร์ตี้คาดหวังรายการการจับคู่แอตทริบิวต์ ซึ่งเป็นออบเจ็กต์ที่มีคีย์เป็นชื่อแอตทริบิวต์ และค่าเป็นรายการองค์ประกอบเป้าหมายหรือไวลด์การ์ด *

const str = `<span id=foo class=bar style="color: red">hello</span>`

$div.setHTML(str)
// <div><span id="foo" class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["span"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["p"]}}) })
// <div><span>hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {"style": ["*"]}}) })
// <div><span style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({dropAttributes: {"id": ["span"]}}) })
// <div><span class="bar" style="color: red">hello</span></div>

$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// <div>hello</div>

allowCustomElements เป็นตัวเลือกในการอนุญาตหรือปฏิเสธองค์ประกอบที่กำหนดเอง หากได้รับอนุญาต การกำหนดค่าอื่นๆ สำหรับองค์ประกอบและแอตทริบิวต์จะยังคงมีผล

const str = `<custom-elem>hello</custom-elem>`

$div.setHTML(str)
// <div></div>

const sanitizer = new Sanitizer({
  allowCustomElements: true,
  allowElements: ["div", "custom-elem"]
})
$div.setHTML(str, { sanitizer })
// <div><custom-elem>hello</custom-elem></div>

แพลตฟอร์ม API

การเปรียบเทียบกับ DOMPurify

DOMPurify เป็นไลบรารีที่รู้จักกันดีซึ่งมีฟังก์ชันการทำงานของการล้างข้อมูล ความแตกต่างหลักระหว่าง Sanitizer API กับ DOMPurify คือ DOMPurify จะแสดงผลลัพธ์ของการล้างข้อมูลเป็นสตริง ซึ่งคุณต้องเขียนลงในองค์ประกอบ DOM ด้วย .innerHTML

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sanitized
// `<em>hello world</em><img src="">`

DOMPurify สามารถใช้เป็นตัวเลือกสำรองได้เมื่อเบราว์เซอร์ไม่ได้ใช้ Sanitizer API

การใช้ DOMPurify มีข้อเสีย 2 ประการ หากระบบแสดงผลสตริง DOMPurify และ .innerHTML จะแยกวิเคราะห์สตริงอินพุต 2 ครั้ง การแยกวิเคราะห์ 2 ครั้งนี้ทำให้เสียเวลาในการประมวลผล แต่ยังอาจทำให้เกิดช่องโหว่ที่น่าสนใจซึ่งเกิดจากกรณีที่ผลลัพธ์ของการแยกวิเคราะห์ครั้งที่ 2 แตกต่างจากครั้งแรก

HTML ยังต้องมีบริบท เพื่อแยกวิเคราะห์ ตัวอย่างเช่น <td> มีความหมายใน <table> แต่ไม่มีความหมายใน <div> เนื่องจาก DOMPurify.sanitize() รับเฉพาะสตริงเป็นอาร์กิวเมนต์ จึงต้องมีการคาดเดาบริบทการแยกวิเคราะห์

Sanitizer API ปรับปรุงแนวทางของ DOMPurify และได้รับการออกแบบมาเพื่อไม่จำเป็นต้องแยกวิเคราะห์ 2 ครั้งและเพื่อชี้แจงบริบทการแยกวิเคราะห์

สถานะ API และการรองรับเบราว์เซอร์

Sanitizer API อยู่ระหว่างการหารือในกระบวนการกำหนดมาตรฐาน และ Chrome อยู่ในระหว่างการใช้ API นี้

ขั้นตอน สถานะ
1. สร้างวิดีโออธิบาย เสร็จสมบูรณ์
2. สร้างข้อกำหนดเฉพาะฉบับร่าง เสร็จสมบูรณ์
3. รวบรวมความคิดเห็นและทำซ้ำการออกแบบ เสร็จสมบูรณ์
4. ช่วงทดลองใช้จากต้นทางของ Chrome เสร็จสมบูรณ์
5. เปิดตัว ตั้งใจจะเปิดตัวใน M105

Mozilla: พิจารณาว่าข้อเสนอนี้ คุ้มค่าที่จะสร้างต้นแบบ และกำลัง ดำเนินการใช้ข้อเสนอนี้อยู่

WebKit: ดูคำตอบในรายชื่ออีเมลของ WebKit

วิธีเปิดใช้ Sanitizer API

Browser Support

  • Chrome: 146.
  • Edge: 146.
  • Firefox: 148.
  • Safari: not supported.

Source

Chrome อยู่ในระหว่างการใช้ Sanitizer API ใน Chrome 93 ขึ้นไป คุณสามารถลองใช้ลักษณะการทำงานได้โดยเปิดใช้แฟล็ก about://flags/#enable-experimental-web-platform-features ใน Chrome Canary และเวอร์ชันที่กำลังพัฒนา เวอร์ชันก่อนหน้า คุณสามารถเปิดใช้ได้ด้วย --enable-blink-features=SanitizerAPI ดูวิธีการเรียกใช้ Chrome ด้วยแฟล็ก

Firefox

Firefox ยังใช้ Sanitizer API เป็นฟีเจอร์ทดลองด้วย หากต้องการเปิดใช้ ให้ตั้งค่าแฟล็ก dom.security.sanitizer.enabled เป็น true ใน about:config

การตรวจหาฟีเจอร์

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

ความคิดเห็น

หากคุณลองใช้ API นี้และมีความคิดเห็น โปรดแจ้งให้เราทราบ แชร์ความคิดเห็นเกี่ยวกับ ปัญหาของ Sanitizer API ใน GitHub และพูดคุยกับผู้เขียนข้อกำหนดเฉพาะและผู้ที่สนใจ API นี้

หากพบข้อบกพร่องหรือลักษณะการทำงานที่ไม่คาดคิดในการใช้ Chrome โปรดยื่นรายงานข้อบกพร่อง เลือกคอมโพเนนต์ Blink>SecurityFeature>SanitizerAPI และแชร์รายละเอียดเพื่อช่วยผู้ใช้ติดตามปัญหา

สาธิต

หากต้องการดูการทำงานของ Sanitizer API โปรดดู Sanitizer API Playground โดย Mike West:

ข้อมูลอ้างอิง