Sanitizer API ใหม่มีเป้าหมายเพื่อสร้างโปรเซสเซอร์ที่มีประสิทธิภาพสำหรับสตริงที่กำหนดเองเพื่อแทรกลงในหน้าเว็บได้อย่างปลอดภัย
แอปพลิเคชันจัดการกับสตริงที่ไม่น่าเชื่อถืออยู่ตลอดเวลา แต่การแสดงเนื้อหาดังกล่าวอย่างปลอดภัยเป็นส่วนหนึ่งของเอกสาร HTML อาจเป็นเรื่องยาก หากไม่ระมัดระวังให้ดี ก็อาจสร้างโอกาสให้เกิด Cross-site Scripting (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="">
ปรับแต่งผ่านการกำหนดค่า
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>
คุณยังควบคุมได้ว่า Sanitizer จะอนุญาตหรือปฏิเสธแอตทริบิวต์ที่ระบุด้วยตัวเลือกต่อไปนี้
allowAttributesdropAttributes
พร็อพเพอร์ตี้ 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 และออกแบบมาเพื่อลดความจำเป็นในการแยกวิเคราะห์ซ้ำและเพื่อชี้แจงบริบทการแยกวิเคราะห์
สถานะ API และการรองรับเบราว์เซอร์
Sanitizer API อยู่ระหว่างการหารือในกระบวนการกำหนดมาตรฐาน และ Chrome อยู่ในขั้นตอนการใช้งาน
| ขั้นตอน | สถานะ |
|---|---|
| 1. สร้างคำอธิบาย | เสร็จสมบูรณ์ |
| 2. สร้างฉบับร่างข้อกำหนด | เสร็จสมบูรณ์ |
| 3. รวบรวมความคิดเห็นและทำซ้ำการออกแบบ | เสร็จสมบูรณ์ |
| 4. ช่วงทดลองใช้จากต้นทางของ Chrome | เสร็จสมบูรณ์ |
| 5. เปิดตัว | Intent to Ship on M105 |
Mozilla: เห็นว่าข้อเสนอนี้ควรสร้างต้นแบบ และกำลังนำไปใช้จริง
WebKit: ดูคำตอบในรายชื่ออีเมลของ WebKit
วิธีเปิดใช้ Sanitizer API
Browser Support
การเปิดใช้ผ่านตัวเลือก about://flags หรือ CLI
Chrome
Chrome อยู่ในขั้นตอนการใช้งาน Sanitizer API ใน Chrome 93 ขึ้นไป คุณสามารถลองใช้ลักษณะการทำงานนี้ได้โดยเปิดใช้about://flags/#enable-experimental-web-platform-features Flag ใน 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
ข้อมูลอ้างอิง
- ข้อกำหนดของ HTML Sanitizer API
- ที่เก็บ WICG/sanitizer-api
- คำถามที่พบบ่อยเกี่ยวกับ Sanitizer API
- เอกสารประกอบข้อมูลอ้างอิงของ HTML Sanitizer API ใน MDN
รูปภาพโดย Towfiqu barbhuiya ใน Unsplash