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

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

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

זה בהחלט נכון לגבי טקסט. אפשר לנסות להדביק את הקוד הבא בכלי הפיתוח מעבירים את המיקוד למרכז וממקדים מחדש באופן מיידי. (הsetTimeout() נדרש כך שיש לכם מספיק זמן למקד את הדף, וזה דרישה של האסינכרוני 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. אם אתם שולטים גם בהעתקה וגם בסוף ההדבקה, לדוגמה, אם מעתיקים מתוך האפליקציה שלך כדי להדביק את התוכן באפליקציה, עליך להשתמש פורמטים מותאמים אישית לאינטרנט עבור API של הלוח האסינכרוני. אפשר להפסיק לקרוא כאן ולעיין במאמר המקושר.
  2. אם אתם שולטים רק בסוף ההדבקה באפליקציה, אבל לא בסוף ההעתקה, אולי כי פעולת ההעתקה מתבצעת באפליקציה נייטיב שלא תומכת עליך להשתמש באפשרות unsanitized, שהיא מוסבר בהמשך המאמר.

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

העתקה והדבקה של HTML לא מאובטח

כשwrite() (מעתיקים) את קוד ה-HTML ללוח באמצעות ה-API של הלוח האסינכרוני, הדפדפן מוודא שהוא תקין על ידי הרצתו דרך מנתח DOM וסריאליזציה של מחרוזת ה-HTML שמתקבלת, אבל לא מתבצע חיטוי השלב הזה. לא צריך לעשות שום דבר. כשמציבים קוד HTML אחד (read()) לוח העריכה של אפליקציה אחרת, ואפליקציית האינטרנט שלך מביעה הסכמה לקבלת אם אתם צריכים לבצע פעולות ניקיון בקוד שלכם, אפשר להעביר אובייקט אפשרויות ל-method 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 רייצ'ל אנדרו. ממשק ה-API צוין הטמענו על ידי צוות Microsoft Edge.