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

תומאס סטיינר
תומאס סטיינר

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

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

זה נכון בהחלט לגבי טקסט. נסו להדביק את הקוד הבא במסוף כלי הפיתוח ולאחר מכן מקדו מחדש את הדף באופן מיידי. (הערך setTimeout() נחוץ כדי שיהיה לכם מספיק זמן להתמקד בדף, פרט לדרישה של Async Clipboard 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. אם אתם שולטים גם בהעתקה וגם בסיום ההדבקה, לדוגמה, אם אתם מעתיקים מהאפליקציה ואז מדביקים אותה בתוך האפליקציה, כדאי להשתמש ב-Web custom formats for the 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. כשתנסו את זה במסוף DevTools, תראו שהקלט והפלט זהים.

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 קיימת.

אישורים

המאמר הזה נכתב על ידי אנופם סניגדה ורייצ'ל אנדרו. צוות Microsoft Edge הגדיר והטמיע את ה-API.