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

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

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, שבהם מחרוזות לא מוצפנות הן מקור אופייני ל-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' (בריחה) מתייחס להחלפה של תווים מיוחדים ב-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="">

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

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

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: נחשבת כדאית ליצירת אב טיפוס, ו-Mozilla מיישמת אותה באופן פעיל.

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

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

Browser Support

  • Chrome: not supported.
  • Edge: not supported.
  • Firefox: 147.
  • Safari: not supported.

הפעלה באמצעות 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 Playground של Mike West:

קובצי עזר


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