קוד HTML לא מאובטח ב-Async Clipboard API

החל מגרסה 120 של Chrome, אפשרות חדשה בשם unsanitized זמינה ב-Async Clipboard API. האפשרות הזו יכולה לעזור במצבים מיוחדים עם HTML, שבהם צריך להדביק את תוכן לוח העריכה באופן זהה לאופן שבו הוא הועתק. כלומר, ללא שלב ביניים של טיהור, שדפדפנים מחילים בדרך כלל – ומסיבות טובות. במדריך הזה מוסבר איך משתמשים בו.

כשעובדים עם Async Clipboard API, ברוב המקרים המפתחים לא צריכים לדאוג לתקינות התוכן בלוח העריכה, וניתן להניח שהם יקבלו אותו תוכן שהם כתבו בלוח העריכה (העתקה) כשהם יקראו את הנתונים מהלוח (הדבקה).

זה בהחלט נכון לגבי טקסט. אפשר לנסות להדביק את הקוד הבא במסוף DevTools ואז להתמקד בדף באופן מיידי. (ה-setTimeout() נדרש כדי שיהיה לכם מספיק זמן למקד את הדף, שהוא דרישה של ה-API של הלוח האסינכרוני). כפי שרואים, הקלט זהה בדיוק לפלט.

setTimeout(async () => {
  const input = 'Hello';
  await navigator.clipboard.writeText(input);
  const output = await navigator.clipboard.readText();
  console.log(input, output, input === output);
  // Logs "Hello Hello true".
}, 3000);

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

setTimeout(async () => {
  const dataURL =
    'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=';
  const input = await fetch(dataURL).then((response) => response.blob());
  await navigator.clipboard.write([
    new ClipboardItem({
      [input.type]: input,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const output = await clipboardItem.getType(input.type);
  console.log(input.size, output.size, input.type === output.type);
  // Logs "68 161 true".
}, 3000);

אבל מה קורה עם טקסט HTML? כפי שאפשר לנחש, המצב שונה ב-HTML. כאן הדפדפן מנקה את קוד ה-HTML כדי למנוע דברים רעים, למשל על ידי הסרת תגי <script> מקוד ה-HTML (ותגים אחרים כמו <meta>,‏ <head> ו-<style>) והוספת CSS בקוד. כדאי לנסות את הדוגמה הבאה ב'מסוף כלי הפיתוח'. תוכלו לראות שהפלט שונה משמעותית מהקלט.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read();
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

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

  1. אם אתם שולטים גם בקצה ההעתקה וגם בקצה הדבקה, למשל, אם אתם מעתיקים מתוך האפליקציה כדי להדביק אותה באותה דרך בתוך האפליקציה, כדאי להשתמש בפורמטים מותאמים אישית לאינטרנט ב-Async Clipboard API. כדאי להפסיק לקרוא כאן ולבדוק את המאמר המקושר.
  2. אם אתם שולטים רק בקצה ההדבקה באפליקציה, אבל לא בקצה ההעתקה, אולי כי פעולת ההעתקה מתבצעת באפליקציה מקורית שלא תומכת בפורמטים מותאמים אישית לאינטרנט, עליכם להשתמש באפשרות unsanitized שמפורטת בהמשך המאמר.

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

העתקה והדבקה של קוד HTML לא מסונן

כשאתם write() (מעתיקים) קוד HTML ללוח באמצעות Async Clipboard API, הדפדפן מוודא שהוא תקין על ידי הרצתו באמצעות מנתח DOM וסריאליזציה של מחרוזת ה-HTML שנוצרת, אבל לא מתבצעת ניטרול זיהומים בשלב הזה. אין צורך לעשות שום דבר. כשאתם read() HTML שמונח בלוח העריכה על ידי אפליקציה אחרת, ואפליקציית האינטרנט שלכם בוחרת לקבל את התוכן ברמת הנאמנות המלאה וצריך לבצע סינון בקוד שלכם, תוכלו להעביר אובייקט אפשרויות לשיטה read() עם המאפיין unsanitized והערך ['text/html']. בנפרד, זה נראה כך: navigator.clipboard.read({ unsanitized: ['text/html'] }). דוגמת הקוד הבאה היא כמעט זהה לדוגמה שצוינה קודם, אבל הפעם עם האפשרות unsanitized. כשתנסו את זה במסוף כלי הפיתוח, תראו שהקלט והפלט זהים.

setTimeout(async () => {
  const input = `<html>  
  <head>  
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />  
    <meta name="ProgId" content="Excel.Sheet" />  
    <meta name="Generator" content="Microsoft Excel 15" />  
    <style>  
      body {  
        font-family: HK Grotesk;  
        background-color: var(--color-bg);  
      }  
    </style>  
  </head>  
  <body>  
    <div>hello</div>  
  </body>  
</html>`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html'],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  console.log(input, output);
}, 3000);

תמיכה בדפדפנים וזיהוי תכונות

אין דרך ישירה לבדוק אם יש תמיכה בתכונה, ולכן זיהוי התכונה מבוסס על התבוננות בהתנהגות. לכן, הדוגמה הבאה מתבססת על זיהוי העובדה אם תג <style> נשאר, מה שמציין תמיכה, או אם הוא מוטמע, מה שמציין שאין תמיכה. שימו לב שכדי שזה יעבוד, לדף כבר צריכה להיות הרשאה ללוח העריכה.

const supportsUnsanitized = async () => {
  const input = `<style>p{color:red}</style><p>a`;
  const inputBlob = new Blob([input], { type: 'text/html' });
  await navigator.clipboard.write([
    new ClipboardItem({
      'text/html': inputBlob,
    }),
  ]);
  const [clipboardItem] = await navigator.clipboard.read({
    unsanitized: ['text/html],
  });
  const outputBlob = await clipboardItem.getType('text/html');
  const output = await outputBlob.text();
  return /<style>/.test(output);
};

הדגמה (דמו)

כדי לראות את האפשרות unsanitized בפעולה, אפשר לעיין בדמו ב-Glitch ובקוד המקור שלו.

מסקנות

כפי שמתואר במבוא, רוב המפתחים אף פעם לא יצטרכו לדאוג לגבי חיטוי בלוח העריכה, והם יוכלו פשוט לעבוד עם אפשרויות הניקיון שמוגדרות כברירת מחדל בדפדפן. במקרים הנדירים שבהם המפתחים צריכים לדאוג לזה, האפשרות unsanitized קיימת.

תודות

המאמר הזה נבדק על ידי Anupam Snigdha ו-Rachel Andrew. צוות Microsoft Edge הגדיר ושילב את ה-API.