HTML ที่ไม่ผ่านการตรวจสอบใน API คลิปบอร์ดแบบไม่พร้อมกัน

ตั้งแต่ Chrome 120 เป็นต้นไป ตัวเลือก unsanitized ใหม่พร้อมให้ใช้งานใน API คลิปบอร์ดแบบไม่พร้อมกัน ตัวเลือกนี้สามารถช่วยในกรณีพิเศษด้วย HTML ซึ่งคุณต้องวางเนื้อหาของคลิปบอร์ดให้เหมือนกับตอนที่คัดลอกมา กล่าวคือ ไม่ต้องใช้ขั้นตอนการทำความสะอาดข้อมูลขั้นกลางที่เบราว์เซอร์มักใช้กัน และเพื่อเหตุผลที่ดี ดูวิธีใช้งานได้ในคู่มือนี้

เมื่อใช้ Async Clipboard API ในกรณีส่วนใหญ่ นักพัฒนาซอฟต์แวร์ไม่จำเป็นต้องกังวลเกี่ยวกับความสมบูรณ์ของเนื้อหาในคลิปบอร์ดและสรุปได้ว่าเนื้อหาที่เขียนลงในคลิปบอร์ด (สำเนา) คือสิ่งที่เดียวกันจะได้รับเมื่ออ่านข้อมูลจากคลิปบอร์ด (วาง)

กรณีนี้เป็นจริงสำหรับข้อความ ลองวางโค้ดต่อไปนี้ในคอนโซล DevTools จากนั้นปรับโฟกัสหน้านั้นใหม่ทันที (จำเป็นต้องใช้ 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 ในและเอาต์พุตมีความสำคัญต่อการทำงานที่ถูกต้องของแอป ในสถานการณ์เช่นนี้ คุณมี 2 ตัวเลือก ได้แก่

  1. ตัวอย่างเช่น หากคุณควบคุมทั้งการคัดลอกและจุดสิ้นสุดการวาง เช่น หากคัดลอกจากภายในแอปเพื่อวางภายในแอปเช่นเดียวกัน คุณควรใช้รูปแบบเว็บที่กำหนดเองสำหรับ Async Clipboard API หยุดอ่านที่นี่และตรวจสอบบทความที่ลิงก์
  2. หากคุณควบคุมเฉพาะส่วนสุดท้ายของการวางในแอป แต่ไม่อนุญาตให้คัดลอก อาจเป็นเพราะการดำเนินการคัดลอกเกิดขึ้นในแอปที่มาพร้อมเครื่องที่ไม่รองรับรูปแบบเว็บที่กำหนดเอง คุณควรใช้ตัวเลือก unsanitized ซึ่งอธิบายไว้ในส่วนที่เหลือของบทความนี้

การทำความสะอาดรวมถึงสิ่งต่างๆ เช่น การนำแท็ก script ออก รูปแบบในบรรทัด และการดูแลให้ HTML มีรูปแบบที่ถูกต้อง รายการนี้ไม่ครอบคลุมและเราอาจเพิ่มขั้นตอนอื่นๆ ในอนาคต

คัดลอกและวาง HTML ที่ไม่ผ่านการตรวจสอบ

เมื่อคุณ write() (คัดลอก) HTML ไปยังคลิปบอร์ดด้วย API คลิปบอร์ดแบบไม่พร้อมกัน เบราว์เซอร์จะตรวจสอบว่ามีการจัดรูปแบบอย่างถูกต้องโดยการเรียกใช้ผ่านโปรแกรมแยกวิเคราะห์ DOM และเรียงอันดับสตริง HTML ที่ได้ แต่ขั้นตอนนี้ไม่มีการทำความสะอาดเกิดขึ้น คุณไม่ต้องดำเนินการใดๆ เมื่อนำ HTML ของ read() ไปวางในคลิปบอร์ดโดยแอปพลิเคชันอื่น และเว็บแอปเลือกรับเนื้อหาที่มีความแม่นยำทั้งหมด และจำเป็นต้องดำเนินการทำความสะอาดในโค้ดของคุณเอง คุณสามารถส่งออบเจ็กต์ตัวเลือกไปยังเมธอด 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