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

ตั้งแต่ Chrome เวอร์ชัน 120 ตัวเลือก unsanitized ใหม่จะพร้อมใช้งานใน Async Clipboard 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 ไปยังคลิปบอร์ดด้วย Async Clipboard API เบราว์เซอร์จะตรวจสอบว่า HTML อยู่ในรูปแบบที่ถูกต้องโดยเรียกใช้ผ่านโปรแกรมแยกวิเคราะห์ 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 อยู่

ขอขอบคุณ

บทความนี้ได้รับการตรวจสอบโดย Anupam Snigdha และ Rachel Andrew ทีม Microsoft Edge เป็นผู้ระบุและติดตั้งใช้งาน API