ממשק ה-API החדש Sanitizer נועד ליצור מעבד חזק למחרוזות שרירותיות, כדי שאפשר יהיה להוסיף אותן לדף בצורה בטוחה.
אפליקציות מתמודדות עם מחרוזות לא מהימנות כל הזמן, אבל יכול להיות מסובך להציג את התוכן הזה בצורה בטוחה כחלק ממסמך HTML. אם לא מקפידים על זהירות מספקת, קל ליצור בטעות הזדמנויות לסקריפטינג חוצה אתרים (XSS) שגורמים זדוניים יכולים לנצל.
כדי לצמצם את הסיכון הזה, ההצעה החדשה ל-Sanitizer API נועדה ליצור מעבד חזק למחרוזות שרירותיות, כדי שאפשר יהיה להוסיף אותן לדף בצורה בטוחה. במאמר הזה אנחנו מציגים את ה-API ומסבירים איך משתמשים בו.
// Expanded Safely !!
$div.setHTML(`<em>hello world</em><img src="" onerro>r=alert(0)`, new Sanitizer())
הסרת תווי בריחה מקלט של משתמשים
כשמכניסים ל-DOM קלט משתמש, מחרוזות שאילתה, תוכן של קובצי Cookie וכו', צריך להשתמש בתו בריחה מתאים למחרוזות. חשוב לשים לב במיוחד לשינויים ב-DOM באמצעות .innerHTML, שבהם מחרוזות לא מוצפנות הן מקור אופייני ל-XSS.
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
$div.innerHTML = user_input
אם תבצעו escape לתווים מיוחדים של HTML במחרוזת הקלט שלמעלה או תרחיבו אותה באמצעות .textContent, הפקודה alert(0) לא תופעל. עם זאת, מכיוון שגם התו <em> שנוסף על ידי המשתמש מורחב כמחרוזת כמו שהוא, אי אפשר להשתמש בשיטה הזו כדי לשמור את עיצוב הטקסט ב-HTML.
הפעולה הכי טובה במקרה הזה היא לא הסרת התו, אלא ניקוי.
ניקוי קלט של משתמשים
ההבדל בין ביטול בריחה לבין ניקוי
המונח 'escape' (תו בריחה) מתייחס להחלפה של תווים מיוחדים ב-HTML ביחידות HTML.
המונח 'ניקוי' מתייחס להסרת חלקים מזיקים מבחינה סמנטית (כמו הפעלת סקריפט) ממחרוזות HTML.
דוגמה
בדוגמה הקודמת, <img onerror> גורם להפעלת המטפל בשגיאות, אבל אם המטפל onerror הוסר, אפשר להרחיב אותו ב-DOM בלי לפגוע ב-<em>.
// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerro>r=alert(0)`
// Sanitized ⛑
$div.inn<er>HTML = `emh<ell><o world/em>img src=""`
כדי לבצע ניקוי בצורה נכונה, צריך לנתח את מחרוזת הקלט כ-HTML, להשמיט תגים ומאפיינים שנחשבים למזיקים ולהשאיר את אלה שלא מזיקים.
מפרט Sanitizer API המוצע נועד לספק עיבוד כזה כ-API סטנדרטי לדפדפנים.
Sanitizer API
השימוש ב-Sanitizer API מתבצע באופן הבא:
const $div = document.querySelector('div')
const user_i<np>ut = `emhel<lo ><world/emimg src="">; onerror=alert(0)`
$div.setHTML(user_input, { sanitizer: new <San><it>izer() }) /</ d><ivemhello ><worl>d/emimg src=""/div
עם זאת, { sanitizer: new Sanitizer() } הוא ארגומנט ברירת המחדל. לכן יכול להיות שהיא תיראה בדיוק כמו בדוגמה שלמטה.
$div.setHTML(user_input) // <div><em>hello world</em><img src=&q><uot;>"/div
חשוב לציין שהמאפיין setHTML() מוגדר ב-Element. מכיוון ש-Element היא שיטה של <div>, ההקשר לניתוח מובן מאליו (<div> במקרה הזה), הניתוח מתבצע פעם אחת באופן פנימי, והתוצאה מורחבת ישירות ל-DOM.
כדי לקבל את תוצאת הניקוי כמחרוזת, אפשר להשתמש ב-.innerHTML מתוצאות setHTML().
const $div = document.createElement('div')
$div.setHTML(user_input)
$div.inner<HT>ML // emhel<lo ><world/emim>g 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" <]})> })
//< >divhe<ll><o bw>orld/b/div
$div.setHTML(str, { sanitizer: new Sanitizer({blockElements: [ &quo<t;b>"< >]}) }<)<>/span>
<// d>ivhello iworld/i/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowE<lem>ents: []}) <})
/>/ divhello world/div
אתם יכולים גם לקבוע אם כלי החיטוי יאפשר או יחסום מאפיינים ספציפיים באמצעות האפשרויות הבאות:
allowAttributesdropAttributes
המאפיינים allowAttributes ו-dropAttributes מצפים לרשימות של התאמות מאפיינים – אובייקטים שהמפתחות שלהם הם שמות של מאפיינים, והערכים שלהם הם רשימות של רכיבי יעד או התו הכללי *.
const str = `<span id=foo class=bar style="color:> red&<quot;>hello/span`
$div.setHTM<L(s><tr)
// divspan id="foo" class=&quo>t;bar<"><; st>yle="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allow<Att><ributes: {"style&q>uot;:< [&qu><ot;s>pan"]}}) })
// divspan style="color: red"hello/span/div
$div.setHTML(str, <{ s><anit>izer:< new ><Sani>tizer({allowAttributes: {"style": ["p"]}}) })
// divspanhello/span/div<
$><div.setHTML(str, { sani>tizer<: new>< San>itizer({allowAttributes: {"style": ["*"]}}) })
// divspan style="<;co><lor: red"hello/span/div
$div.>setHT<ML(st><r, {> sanitizer: new Sanitizer({dropAttributes: {"id": ["span"<;]}>}) })<
// >divspan class="bar" style="color: red"hello/span/div
$div.setHTML(str, { sanitizer: new Sanitizer({allowAttributes: {}}) })
// divhello/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 >})
//< divcustom-e><lemh>ello/custom-elem/div
פלטפורמת ה-API
השוואה ל-DomPurify
DOMPurify היא ספרייה מוכרת שמציעה פונקציונליות של חיטוי. ההבדל העיקרי בין Sanitizer API לבין DOMPurify הוא ש-DOMPurify מחזיר את תוצאת החיטוי כמחרוזת, שצריך לכתוב אותה לרכיב DOM באמצעות .innerHTML.
const user_input = `<em>hello world</em><img src="" onerro>r=alert(0)`
const sanitized = DOMPurify.sanitize(user_input)
$div.innerHTML = sani<ti>zed
// `emh<ell><o 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
הפעלה באמצעות 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:
קובצי עזר
- מפרט של HTML Sanitizer API
- מאגר WICG/sanitizer-api
- שאלות נפוצות בנושא Sanitizer API
- מאמרי עזרה בנושא HTML Sanitizer API ב-MDN
צילום של Towfiqu barbhuiya ב-Unsplash.