טיפול בטוח ב-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, כאשר מחרוזות ללא בריחה (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.

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

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

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

תו בריחה (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 סטנדרטי לדפדפנים.

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

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

Chrome

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

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

הדגמה (דמו)

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

קובצי עזר


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