טיפול בטוח ב-DOM באמצעות Sanitizer API

המטרה של הממשק החדש של Sanitizer API היא לפתח מעבד מידע חזק שאפשר להוסיף אליו בבטחה מחרוזות שרירותיות.

Jack J
Jack J

אפליקציות מטפלות במחרוזות לא מהימנות כל הזמן, אבל עיבוד בטוח של התוכן הזה כחלק ממסמך HTML עלול להיות מורכב. אם לא תקפידו על כך, קל ליצור בטעות הזדמנויות לכתיבת סקריפטים חוצי-אתרים (XSS) שתוקפים זדוניים עלולים לנצל.

כדי למזער את הסיכון, ההצעה החדשה של Sanitizer API נועדה ליצור מעבד חזק למחרוזות שרירותיות להוספה בטוחה לדף. המאמר הזה מציג את ה-API ומסביר את השימוש בו.

// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerror=alert(0)>`, new Sanitizer())

קלט של משתמשים בתהליכי בריחה

כשמוסיפים ל-DOM קלט של משתמשים, מחרוזות שאילתה, תוכן של קובצי cookie וכן הלאה, צריך לסמן את המחרוזות בתו בריחה (escape) בצורה תקינה. יש לשים לב במיוחד למניפולציית DOM דרך .innerHTML, כאשר מחרוזות ללא בריחה (escape) הן מקור אופייני ל-XSS.

const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
$div.innerHTML = user_input

אם מוסיפים תו בריחה (escape) לתווים מיוחדים של HTML במחרוזת הקלט שלמעלה, או מרחיבים אותה באמצעות .textContent, לא תתבצע הרצה של alert(0). עם זאת, מכיוון שהמשתמש <em> הוסיף אותו הוא גם מורחב כמחרוזת כפי שהיא, אי אפשר להשתמש בשיטה הזו כדי לשמור את עיטור הטקסט ב-HTML.

הדרך הטובה ביותר לעשות זאת היא לא להבריח, אלא לנקות את הראש.

מתבצע חיטוי של הקלט מהמשתמשים

ההבדל בין בריחה (escape) לבין חיטוי

סימון בתו בריחה (escape) מתייחס להחלפה של תווי HTML מיוחדים בישויות HTML.

חיטוי מתייחס להסרת חלקים מזיקים מבחינה סמנטית (למשל ביצוע סקריפט) ממחרוזות HTML.

דוגמה

בדוגמה הקודמת, <img onerror> גורם להרצת ה-handler של השגיאות, אבל אם ה-handler של 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. אם מוחזרת מחרוזת, אז מחרוזת הקלט מנותחת פעמיים, על ידי DOMPurify ו-.innerHTML. הניתוח הכפול הזה מבזבז זמן עיבוד, אבל יכול גם להוביל לנקודות חולשה מעניינות שנגרמו כתוצאה ממקרים שבהם התוצאה של הניתוח השני שונה מהראשונה.

ל-HTML נדרש גם הקשר כדי לנתח אותו. לדוגמה, <td> הגיוני ב<table> אבל לא ב<div>. מכיוון ש-DOMPurify.sanitize() לוקחת רק מחרוזת כארגומנט, היה צורך לנחש את הקשר הניתוח.

Sanitizer API משפר את גישת DOMPurify ונועד לבטל את הצורך בניתוח כפול ולהבהיר את ההקשר של הניתוח.

סטטוס ה-API ותמיכה בדפדפן

ממשק ה-Sanitizer API נמצא בדיון בתהליך הסטנדרטיזציה ו-Chrome נמצא בתהליך ההטמעה.

שלב סטטוס
1. יצירת הסבר הושלם
2. יצירת טיוטה של מפרט הושלם
3. אוספים משוב וחוזרים על העיצוב הושלם
4. גרסת המקור לניסיון ב-Chrome הושלם
5. הפעלה הכוונה למשלוח בגרסה M105

Mozilla: מתייחסת להצעה הזו בשווי של יצירת אב הטיפוס, ומיישמת אותה באופן פעיל.

WebKit: ראה את התגובה ברשימת התפוצה של WebKit.

איך מפעילים את ממשק ה-API לחיטוי

תמיכה בדפדפן

  • Chrome: לא נתמך.
  • Edge: לא נתמך.
  • Firefox: מאחורי דגל.
  • Safari: לא נתמך.

מקור

הפעלה דרך about://flags או אפשרות CLI

Chrome

Chrome נמצא בתהליך ההטמעה של Sanitizer API. ב-Chrome בגרסה 93 ואילך, אפשר לנסות את ההתנהגות על ידי הפעלת התכונה הניסיונית about://flags/#enable-experimental-web-platform-features. בגרסאות קודמות של Chrome Canary וערוץ Dev, אפשר להפעיל אותו דרך --enable-blink-features=SanitizerAPI ולנסות עכשיו. הוראות להפעלת Chrome עם דגלים

Firefox

Firefox מיישם גם את Sanitizer API כתכונה ניסיונית. כדי להפעיל אותה, צריך להגדיר את הדגל 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 ב-Unbounce.