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

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

Jack J
Jack J

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

เพื่อลดความเสี่ยงดังกล่าว ข้อเสนอ Sanitizer API ใหม่จึงมีเป้าหมายในการสร้างตัวประมวลผลที่มีประสิทธิภาพสำหรับสตริงที่กำหนดเองเพื่อแทรกลงในหน้าเว็บอย่างปลอดภัย บทความนี้จะแนะนำ 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>) ระบบจะแยกวิเคราะห์ภายในเพียงครั้งเดียว และขยายผลลัพธ์ไปยัง 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 อย่าง หากระบบแสดงผลสตริง ระบบจะแยกวิเคราะห์สตริงอินพุต 2 ครั้งโดย DOMPurify และ .innerHTML การแยกวิเคราะห์ 2 ครั้งนี้ทำให้เสียเวลาในการประมวลผล แต่ก็อาจนำไปสู่ช่องโหว่ที่น่าสนใจซึ่งเกิดจากกรณีที่ผลลัพธ์ของการแยกวิเคราะห์ครั้งที่ 2 แตกต่างจากครั้งแรก

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

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

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

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

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

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

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

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

การเปิดใช้ผ่านตัวเลือก about://flags หรือ CLI

Chrome

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

Firefox

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

การตรวจหาองค์ประกอบ

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

ความคิดเห็น

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

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

สาธิต

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

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


รูปภาพโดย Towfiqu barbhuiya จาก Unsplash