Sanitizer API ใหม่มีเป้าหมายเพื่อสร้างโปรเซสเซอร์ที่มีประสิทธิภาพสำหรับสตริงที่กำหนดเองเพื่อแทรกลงในหน้าเว็บได้อย่างปลอดภัย
แอปพลิเคชันจัดการกับสตริงที่ไม่น่าเชื่อถืออยู่ตลอดเวลา แต่การแสดงเนื้อหาดังกล่าวอย่างปลอดภัยเป็นส่วนหนึ่งของเอกสาร HTML อาจเป็นเรื่องยาก หากไม่ระมัดระวังให้ดี ก็อาจสร้างโอกาสให้เกิด Cross-site Scripting (XSS) โดยไม่ตั้งใจ ซึ่งผู้โจมตีที่ประสงค์ร้ายอาจใช้ประโยชน์จากช่องโหว่นี้
ข้อเสนอ Sanitizer API ใหม่มีเป้าหมายที่จะสร้างโปรเซสเซอร์ที่มีประสิทธิภาพสำหรับสตริงที่กำหนดเองเพื่อแทรกลงในหน้าเว็บได้อย่างปลอดภัย เพื่อลดความเสี่ยงดังกล่าว บทความนี้จะแนะนํา API และอธิบายการใช้งาน
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
การหลีกเลี่ยงอินพุตของผู้ใช้
เมื่อแทรกข้อมูลที่ผู้ใช้ป้อน สตริงการค้นหา เนื้อหาคุกกี้ และอื่นๆ ลงใน DOM คุณต้องกำหนดสตริงเป็นอักขระหลีกอย่างเหมาะสม ควรให้ความสนใจเป็นพิเศษกับการจัดการ DOM ผ่าน .innerHTML ซึ่งสตริงที่ไม่ได้หลีกเลี่ยงมักเป็นแหล่งที่มาของ XSS
const user_input = `<em>hello world</em><img src="" onerro>r=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="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
หากต้องการล้างข้อมูลอย่างถูกต้อง คุณต้องแยกวิเคราะห์สตริงอินพุตเป็น HTML ละเว้นแท็กและแอตทริบิวต์ที่ถือว่าเป็นอันตราย และเก็บแท็กและแอตทริบิวต์ที่ไม่มีอันตรายไว้
ข้อกำหนดของ Sanitizer API ที่เสนอมีจุดมุ่งหมายเพื่อจัดให้การประมวลผลดังกล่าวเป็น API มาตรฐานสำหรับเบราว์เซอร์
Sanitizer API
Sanitizer API ใช้ในลักษณะต่อไปนี้
const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
แต่ { sanitizer: new Sanitizer() } เป็นอาร์กิวเมนต์เริ่มต้น ดังนั้นจึงอาจมีลักษณะดังด้านล่าง
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
โปรดทราบว่า setHTML() กำหนดไว้ใน Element เนื่องจากเป็นเมธอดของ Element บริบทที่จะแยกวิเคราะห์จึงอธิบายตัวเองได้ (<div> ในกรณีนี้) การแยกวิเคราะห์จะทําเพียงครั้งเดียวภายใน และผลลัพธ์จะขยายลงใน DOM โดยตรง
หากต้องการรับผลลัพธ์ของการล้างข้อมูลเป็นสตริง คุณสามารถใช้ .innerHTML จากผลลัพธ์ setHTML()
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
คุณยังควบคุมได้ว่า Sanitizer จะอนุญาตหรือปฏิเสธแอตทริบิวต์ที่ระบุด้วยตัวเลือกต่อไปนี้
allowAttributesdropAttributes
พร็อพเพอร์ตี้ allowAttributes และ dropAttributes คาดหวังรายการที่ตรงกันของแอตทริบิวต์ ซึ่งเป็นออบเจ็กต์ที่มีคีย์เป็นชื่อแอตทริบิวต์ และค่าเป็นรายการขององค์ประกอบเป้าหมายหรือไวลด์การ์ด *
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div
แพลตฟอร์ม API
การเปรียบเทียบกับ DomPurify
DOMPurify เป็นไลบรารีที่รู้จักกันดีซึ่งมีฟังก์ชันการล้างข้อมูล ความแตกต่างหลักระหว่าง Sanitizer API กับ DOMPurify คือ DOMPurify จะแสดงผลลัพธ์ของการล้างข้อมูลเป็นสตริง ซึ่งคุณต้องเขียนลงในองค์ประกอบ DOM ผ่าน .innerHTML
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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