การควบคุม 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())

การป้อนข้อมูลของผู้ใช้เป็น Escape

เมื่อแทรกอินพุตของผู้ใช้ สตริงการค้นหา เนื้อหาของคุกกี้ และอื่นๆ ลงใน DOM สตริงจะต้องกำหนดเป็นอักขระหลีกอย่างถูกต้อง ควรให้ความสนใจเป็นพิเศษกับการควบคุม DOM ผ่าน .innerHTML ซึ่งสตริงที่ไม่ใช้ Escape เป็นแหล่งที่มาโดยทั่วไปของ 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 มาตรฐานสำหรับเบราว์เซอร์

API ของ Sanitizer

มีการใช้ 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 ต่างจากครั้งแรก

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

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

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

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

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

Mozilla: เห็นว่าข้อเสนอนี้ควรสร้างต้นแบบและนำข้อเสนอนี้ไปใช้อย่างจริงจัง

WebKit: ดูการตอบกลับในรายชื่ออีเมล WebKit

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

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

  • x
  • x
  • x

แหล่งที่มา

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

Chrome

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

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

ข้อมูลประชากร

ดูการทำงานของ Sanitizer API ได้ที่ Sanitizer API Playground โดย Mike West

รายการอ้างอิง


รูปภาพโดย Towfiqu barbhuiya ใน Unsplash