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

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

Jack J
Jack J

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

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

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

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

כשמזינים ל-DOM קלט של משתמש, מחרוזות של שאילתות, תוכן של קובצי cookie וכו', צריך להשתמש בהימלטות (escape) נכונה של המחרוזות. חשוב להקפיד במיוחד על מניעת מניפולציה של 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.

במקרה כזה, עדיף לא לבצע בריחה אלא ניקוי.

טיהור קלט של משתמשים

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

תו בריחה (escape) הוא תו שמחליף תווים מיוחדים ב-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="">

התאמה אישית באמצעות הגדרה

כברירת מחדל, 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.

איך מפעילים את Sanitizer API

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox: behind a flag.
  • Safari: not supported.

הפעלה באמצעות about://flags או אפשרות CLI

Chrome

אנחנו בתהליך הטמעת Sanitizer API ב-Chrome. ב-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 הזה ותהיה לכם משוב, נשמח לשמוע אותו. אתם יכולים לשתף את דעתכם על בעיות ב-GitHub בנושא Sanitizer API ולנהל דיון עם מחברי המפרט ועם אנשים שמתעניינים ב-API הזה.

אם נתקלתם באגים או בהתנהגות לא צפויה בהטמעה של Chrome, דווחו על כך. בוחרים את הרכיבים של Blink>SecurityFeature>SanitizerAPI ומשתפים את הפרטים כדי לעזור למטמיעים לעקוב אחרי הבעיה.

הדגמה (דמו)

כדי לראות את Sanitizer API בפעולה, אפשר להיכנס למגרש המשחקים של Sanitizer API של Mike West:

קובצי עזר


צילום: Towfiqu barbhuiya ב-Unsplash.