การควบคุม 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="" 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;>&quot;/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 จะอนุญาตหรือปฏิเสธแอตทริบิวต์ที่ระบุด้วยตัวเลือกต่อไปนี้

  • allowAttributes
  • dropAttributes

พร็อพเพอร์ตี้ 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

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox Technology Preview: supported.
  • Safari: not supported.

การเปิดใช้ผ่านตัวเลือก 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

ข้อมูลอ้างอิง


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