แอปพลิเคชันต้องจัดการกับสตริงที่ไม่น่าเชื่อถืออยู่เสมอ แต่การแสดงผลเนื้อหาดังกล่าวอย่างปลอดภัยเป็นส่วนหนึ่งของเอกสาร HTML อาจเป็นเรื่องยาก หากไม่ระมัดระวังมากพอ คุณอาจสร้างโอกาสให้เกิด Cross-site Scripting (XSS) โดยไม่ตั้งใจ ซึ่งผู้โจมตีที่มีเจตนาร้ายอาจใช้ประโยชน์จากช่องโหว่นี้ได้
ข้อเสนอ Sanitizer 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> ในกรณีนี้) ระบบจะแยกวิเคราะห์ภายใน 1 ครั้ง และขยายผลลัพธ์ลงใน 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>
นอกจากนี้ คุณยังควบคุมได้ว่าตัวล้างข้อมูลจะอนุญาตหรือปฏิเสธแอตทริบิวต์ที่ระบุด้วยตัวเลือกต่อไปนี้
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 ประการ หากระบบแสดงผลสตริง DOMPurify และ .innerHTML จะแยกวิเคราะห์สตริงอินพุต 2 ครั้ง การแยกวิเคราะห์ 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 อยู่ในระหว่างการใช้ 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 นี้และมีความคิดเห็น โปรดแจ้งให้เราทราบ แชร์ความคิดเห็นเกี่ยวกับ ปัญหาของ Sanitizer API ใน GitHub และพูดคุยกับผู้เขียนข้อกำหนดเฉพาะและผู้ที่สนใจ API นี้
หากพบข้อบกพร่องหรือลักษณะการทำงานที่ไม่คาดคิดในการใช้ Chrome โปรดยื่นรายงานข้อบกพร่อง เลือกคอมโพเนนต์ Blink>SecurityFeature>SanitizerAPI และแชร์รายละเอียดเพื่อช่วยผู้ใช้ติดตามปัญหา
สาธิต
หากต้องการดูการทำงานของ Sanitizer API โปรดดู Sanitizer API Playground โดย Mike West: