טיפול בטוח ב-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' מתייחס להחלפת תווי 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

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

ליישום DOMPurify יש כמה חסרונות. אם מוחזרת מחרוזת, מחרוזת הקלט מנותחת פעמיים: DOMPurify ו-.innerHTML. הניתוח הכפול הזה מבזבז את זמן העיבוד, אבל הוא עלול גם להוביל לנקודות חולשה מעניינות שנגרמות במקרים שבהם התוצאה של הניתוח השני שונה מזו של הראשונה.

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

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

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

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

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

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

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

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

תמיכה בדפדפן

  • x
  • x
  • x

מקור

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

Chrome

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

Firefox

בנוסף, Firefox מטמיע את ממשק ה-API של Sanitizer כתכונה ניסיונית. כדי להפעיל אותו, צריך להגדיר את הדגל dom.security.sanitizer.enabled לערך true ב-about:config.

זיהוי תכונות

if (window.Sanitizer) {
  // Sanitizer API is enabled
}

משוב

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

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

הדגמה (דמו)

כדי לראות את Sanitizer API בפעולה, עיין ב-Sanitizer API Playground מאת Mike West:

קובצי עזר


תמונה מאת Towfiqu barbhuiya ב-UnFlood.