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

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

Jack J
Jack J

แอปพลิเคชันจัดการกับสตริงที่ไม่น่าเชื่อถืออยู่ตลอดเวลา แต่การแสดงผลเนื้อหานั้นอย่างปลอดภัยโดยเป็นส่วนหนึ่งของเอกสาร HTML อาจเป็นเรื่องยุ่งยาก หากไม่มีการดูแลที่เพียงพอ อาจเกิดข้อผิดพลาดโดยไม่ได้ตั้งใจสำหรับ cross-site Scripting (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 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 อยู่ระหว่างการนำ API มาใช้

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

Mozilla: ถือว่าข้อเสนอนี้เป็นการสร้างต้นแบบและนำมาใช้งานจริง

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

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

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

  • Chrome: ไม่รองรับ
  • Edge: ไม่รองรับ
  • Firefox: อยู่หลังธง
  • Safari: ไม่รองรับ

แหล่งที่มา

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

Chrome

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

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