การแยกเว็บไซต์สำหรับนักพัฒนาเว็บ

Chrome 67 บนเดสก์ท็อปมีฟีเจอร์ใหม่ที่เรียกว่าการแยกเว็บไซต์ซึ่งเปิดใช้โดยค่าเริ่มต้น บทความนี้อธิบายความหมายของการแยกเว็บไซต์ เหตุผลที่ต้องใช้ และเหตุผลที่นักพัฒนาเว็บควรทราบ

การแยกเว็บไซต์คืออะไร

อินเทอร์เน็ตมีไว้เพื่อดูวิดีโอแมวและจัดการกระเป๋าสตางค์คริปโตเคอเรนซี รวมถึงสิ่งอื่นๆ แต่คุณคงไม่อยากให้ fluffycats.example มีสิทธิ์เข้าถึงคริปโตเคอเรนซีอันล้ำค่าของคุณ โชคดีที่เว็บไซต์มักเข้าถึงข้อมูลของกันและกันภายในเบราว์เซอร์ไม่ได้เนื่องด้วยนโยบายต้นทางเดียวกัน อย่างไรก็ตาม เว็บไซต์ที่เป็นอันตรายอาจพยายามหลีกเลี่ยงนโยบายนี้เพื่อโจมตีเว็บไซต์อื่นๆ และบางครั้งก็พบข้อบกพร่องด้านความปลอดภัยในโค้ดของเบราว์เซอร์ที่บังคับใช้นโยบายต้นทางเดียวกัน ทีม Chrome มีเป้าหมายที่จะแก้ไขข้อบกพร่องดังกล่าวโดยเร็วที่สุด

การแยกเว็บไซต์เป็นฟีเจอร์ด้านความปลอดภัยใน Chrome ที่ให้การป้องกันอีกชั้นหนึ่งเพื่อลดโอกาสที่การโจมตีดังกล่าวจะสำเร็จ ซึ่งช่วยให้มั่นใจว่าหน้าเว็บจากเว็บไซต์ต่างๆ จะอยู่ในกระบวนการที่แตกต่างกันเสมอ โดยแต่ละหน้าจะทำงานในแซนด์บ็อกซ์ที่จำกัดสิ่งที่กระบวนการได้รับอนุญาตให้ทำ และยังบล็อกไม่ให้กระบวนการรับข้อมูลที่ละเอียดอ่อนบางประเภทจากเว็บไซต์อื่นด้วย ด้วยเหตุนี้ การแยกเว็บไซต์อาจทำให้เว็บไซต์ที่เป็นอันตรายใช้งานการโจมตีแบบ Side-channel ที่ไม่แน่นอน เช่น Spectre เพื่อขโมยข้อมูลจากเว็บไซต์อื่นได้ยากขึ้น เมื่อทีม Chrome บังคับใช้เพิ่มเติมเสร็จแล้ว การแยกเว็บไซต์จะช่วยได้แม้ว่าหน้าของผู้โจมตีจะละเมิดกฎบางอย่างในกระบวนการของตัวเองก็ตาม

การแยกเว็บไซต์ทำให้เว็บไซต์ที่ไม่น่าไว้วางใจเข้าถึงหรือขโมยข้อมูลจากบัญชีของคุณในเว็บไซต์อื่นๆ ได้ยากขึ้น ซึ่งจะเพิ่มการป้องกันข้อบกพร่องด้านความปลอดภัยประเภทต่างๆ เช่น การโจมตีช่องทางข้าง Meltdown และ Spectre เมื่อเร็วๆ นี้

ดูรายละเอียดเพิ่มเติมเกี่ยวกับการแยกเว็บไซต์ได้ในบทความในบล็อกการรักษาความปลอดภัยของ Google

การบล็อกการอ่านข้ามต้นทาง

แม้ว่าหน้าข้ามเว็บไซต์ทั้งหมดจะอยู่ในกระบวนการแยกต่างหาก แต่หน้าเว็บจะยังคงขอทรัพยากรย่อยข้ามเว็บไซต์บางอย่างได้ เช่น รูปภาพและ JavaScript หน้าเว็บที่เป็นอันตรายอาจใช้องค์ประกอบ <img> เพื่อโหลดไฟล์ JSON ที่มีข้อมูลที่ละเอียดอ่อน เช่น ยอดเงินในบัญชีธนาคาร ดังนี้

<img src="https://your-bank.example/balance.json" />
<!-- Note: the attacker refused to add an `alt` attribute, for extra evil points. -->

หากไม่มีการแยกเว็บไซต์ เนื้อหาของไฟล์ JSON จะเข้าสู่หน่วยความจำของกระบวนการแสดงผล เมื่อถึงจุดนี้ โปรแกรมแสดงผลจะพบว่าไฟล์ดังกล่าวไม่ใช่รูปแบบรูปภาพที่ถูกต้องและจะไม่แสดงผลรูปภาพ แต่ผู้โจมตีอาจใช้ประโยชน์จากช่องโหว่อย่าง Spectre เพื่ออ่านข้อมูลหน่วยความจำส่วนนั้น

แทนที่จะใช้ <img> ผู้โจมตีอาจใช้ <script> เพื่อบันทึกข้อมูลที่ละเอียดอ่อนลงในหน่วยความจำได้เช่นกัน

<script src="https://your-bank.example/balance.json"></script>

Cross-Origin Read Block หรือ CORB เป็นฟีเจอร์ความปลอดภัยใหม่ที่ป้องกันไม่ให้เนื้อหาของ balance.json เข้าสู่หน่วยความจำของหน่วยความจำประมวลผลของตัวแสดงผลตามประเภท MIME

มาดูวิธีการทำงานของ CORB กัน เว็บไซต์สามารถขอทรัพยากรได้ 2 ประเภทจากเซิร์ฟเวอร์ ดังนี้

  1. แหล่งข้อมูล เช่น เอกสาร HTML, XML หรือ JSON
  2. ทรัพยากรสื่อ เช่น รูปภาพ, JavaScript, CSS หรือแบบอักษร

เว็บไซต์สามารถรับทรัพยากรข้อมูลจากต้นทางของตนเองหรือจากต้นทางอื่นๆ ด้วยส่วนหัว CORS ที่อนุญาต เช่น Access-Control-Allow-Origin: * ในทางกลับกัน คุณสามารถรวมทรัพยากรสื่อจากต้นทางใดก็ได้ แม้จะไม่มีส่วนหัว CORS ที่อนุญาต

CORB จะป้องกันไม่ให้กระบวนการแสดงผลได้รับทรัพยากรข้อมูลที่มาจากแหล่งที่มาหลายแห่ง (เช่น HTML, XML หรือ JSON) ในกรณีต่อไปนี้

  • ทรัพยากรมีส่วนหัว X-Content-Type-Options: nosniff
  • CORS ไม่อนุญาตให้เข้าถึงทรัพยากรอย่างชัดเจน

หากแหล่งข้อมูลแบบข้ามต้นทางไม่ได้ตั้งค่าส่วนหัว X-Content-Type-Options: nosniff ไว้ CORB จะพยายามตรวจจับเนื้อหาการตอบกลับเพื่อระบุว่าเป็น HTML, XML หรือ JSON ซึ่งจําเป็นเนื่องจากเว็บเซิร์ฟเวอร์บางเครื่องกําหนดค่าไม่ถูกต้องและแสดงรูปภาพเป็น text/html เป็นต้น

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

เราขอแนะนําให้ทําดังนี้เพื่อให้ได้รับความปลอดภัยสูงสุดและใช้ประโยชน์จาก CORB

  • ทำเครื่องหมายคำตอบด้วยส่วนหัว Content-Type ที่ถูกต้อง (เช่น ทรัพยากร HTML ควรแสดงเป็น text/html, ทรัพยากร JSON ที่มีประเภท MIME ของ JSON และทรัพยากร XML ที่มีประเภท MIME ของ XML)
  • เลือกไม่ใช้การสแกนหาโดยใช้ส่วนหัว X-Content-Type-Options: nosniff หากไม่มีส่วนหัวนี้ Chrome จะทำการวิเคราะห์เนื้อหาอย่างรวดเร็วเพื่อพยายามยืนยันว่าประเภทดังกล่าวถูกต้อง แต่เนื่องจากปัญหานี้เกิดควบคู่ไปกับการอนุญาตให้ตอบกลับได้เพื่อหลีกเลี่ยงการบล็อกสิ่งต่างๆ เช่น ไฟล์ JavaScript คุณควรยืนยันในสิ่งที่ถูกต้องด้วยตนเอง

ดูรายละเอียดเพิ่มเติมได้ที่บทความCORB สําหรับนักพัฒนาเว็บ หรือคําอธิบาย CORB โดยละเอียด

เหตุใดนักพัฒนาเว็บจึงควรให้ความสำคัญกับการแยกเว็บไซต์

ส่วนใหญ่แล้ว การแยกเว็บไซต์เป็นฟีเจอร์เบราว์เซอร์ที่ทำงานเบื้องหลัง ซึ่งนักพัฒนาเว็บไม่ได้ใช้งานโดยตรง เช่น ไม่มี API ใหม่ที่แสดงในเว็บให้เรียนรู้ โดยทั่วไป หน้าเว็บไม่ควรแยกความแตกต่างได้เมื่อทำงานโดยมีหรือไม่มีการแยกเว็บไซต์

อย่างไรก็ตาม กฎนี้มีข้อยกเว้นบางประการ การเปิดใช้การแยกเว็บไซต์จะมีผลกระทบเล็กๆ น้อยๆ บางอย่างซึ่งอาจส่งผลต่อเว็บไซต์ของคุณ เราเก็บรวบรวมรายการปัญหาการแยกเว็บไซต์ที่ทราบไว้ และอธิบายปัญหาที่สำคัญที่สุดไว้ด้านล่าง

เลย์เอาต์แบบเต็มหน้าจะไม่ซิงค์กันอีกต่อไป

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

ตัวอย่างเช่น สมมติว่าเว็บไซต์ชื่อ fluffykittens.example สื่อสารกับวิดเจ็ตโซเชียลที่โฮสต์ใน social-widget.example

<!-- https://fluffykittens.example/ -->
<iframe src="https://social-widget.example/" width="123"></iframe>
<script>
  const iframe = document.querySelector('iframe');
  iframe.width = 456;
  iframe.contentWindow.postMessage(
    // The message to send:
    'Meow!',
    // The target origin:
    'https://social-widget.example'
  );
</script>

ในช่วงแรก ความกว้างของ <iframe> วิดเจ็ตโซเชียลคือ 123 พิกเซล แต่จากนั้นหน้า FluffyKittens จะเปลี่ยนความกว้างเป็น 456 พิกเซล (เรียกใช้เลย์เอาต์) และส่งข้อความไปยังวิดเจ็ตโซเชียลซึ่งมีโค้ดต่อไปนี้

<!-- https://social-widget.example/ -->
<script>
  self.onmessage = () => {
    console.log(document.documentElement.clientWidth);
  };
</script>

เมื่อใดก็ตามที่วิดเจ็ตโซเชียลได้รับข้อความผ่าน API ของ postMessage ระบบจะบันทึกความกว้างขององค์ประกอบ <html> รากของวิดเจ็ต

ระบบจะบันทึกค่าความกว้างใด ก่อนที่ Chrome จะเปิดใช้การแยกเว็บไซต์ คำตอบคือ 456 การเข้าถึง document.documentElement.clientWidth จะบังคับเลย์เอาต์ ซึ่งก่อนหน้านี้เป็นแบบซิงโครนัสก่อนการแยกเว็บไซต์ที่เปิดใช้ Chrome อย่างไรก็ตาม เมื่อเปิดใช้การแยกเว็บไซต์ วิดเจ็ตโซเชียลข้ามแหล่งที่มาจะจัดเรียงใหม่แบบไม่สอดคล้องกันในกระบวนการแยกต่างหาก ดังนั้นคําตอบตอนนี้อาจเป็น 123 ก็ได้ ซึ่งก็คือค่า width เดิม

หากหน้าเว็บเปลี่ยนขนาดของ <iframe> ข้ามแหล่งที่มา แล้วส่ง postMessage ไปยัง <iframe> นั้น เฟรมฝั่งที่รับอาจยังไม่ทราบขนาดใหม่เมื่อได้รับข้อความ โดยทั่วไป การทำเช่นนี้อาจทำให้หน้าไม่ทำงานหากเห็นว่าการเปลี่ยนแปลงเลย์เอาต์มีผลกับทุกเฟรมในหน้าทันที

ในตัวอย่างนี้ โซลูชันที่มีประสิทธิภาพมากกว่าจะตั้งค่า width ในเฟรมระดับบนสุด และตรวจจับการเปลี่ยนแปลงดังกล่าวใน <iframe> ด้วยการรอฟังเหตุการณ์ resize

ตัวแฮนเดิลการยกเลิกการโหลดอาจหมดเวลาบ่อยขึ้น

เมื่อกรอบเลื่อนไปมาหรือปิด เอกสารเก่าและเอกสารเฟรมย่อยที่ฝังอยู่ในกรอบนั้นทั้งหมดจะเรียกใช้ตัวแฮนเดิล unload หากการไปยังส่วนใหม่เกิดขึ้นในกระบวนการแสดงผลเดียวกัน (เช่น สำหรับการไปยังส่วนที่มีแหล่งที่มาเดียวกัน) แฮนเดิล unload ของเอกสารเก่าและเฟรมย่อยของเอกสารนั้นอาจทำงานเป็นเวลานานเท่าใดก็ได้ก่อนที่จะอนุญาตให้การไปยังส่วนใหม่ดำเนินการ

addEventListener('unload', () => {
  doSomethingThatMightTakeALongTime();
});

ในกรณีนี้ ตัวแฮนเดิล unload ในเฟรมทั้งหมดมีความน่าเชื่อถือมาก

อย่างไรก็ตาม แม้จะไม่มีการแยกเว็บไซต์ แต่การนําทางเฟรมหลักบางรายการจะข้ามกระบวนการ ซึ่งส่งผลต่อลักษณะการทํางานของตัวแฮนเดิลการยกเลิกการโหลด เช่น หากคุณนำทางจาก old.example ไปยัง new.example ด้วยการพิมพ์ URL ในแถบที่อยู่ การนำทางของ new.example จะเกิดขึ้นในกระบวนการใหม่ แฮนเดิลการยกเลิกโหลดสําหรับ old.example และเฟรมย่อยจะทํางานในกระบวนการ old.example ในเบื้องหลังหลังจากหน้า new.example แสดงขึ้น และระบบจะสิ้นสุดการทำงานของแฮนเดิลการยกเลิกโหลดเดิมหากไม่เสร็จสิ้นภายในระยะหมดเวลาที่กำหนด เนื่องจากตัวแฮนเดิลการยกเลิกการโหลดอาจทำงานไม่เสร็จก่อนที่การหมดเวลา ลักษณะการยกเลิกการโหลดจึงไม่น่าเชื่อถือ

เมื่อใช้การแยกเว็บไซต์ การนำทางข้ามเว็บไซต์ทั้งหมดจะกลายเป็นการนำทางข้ามกระบวนการ เพื่อให้เอกสารจากเว็บไซต์ต่างๆ ใช้กระบวนการเดียวกันไม่ได้ ด้วยเหตุนี้ สถานการณ์ข้างต้นจึงเกิดขึ้นได้บ่อยขึ้น และตัวแฮนเดิลการยกเลิกการโหลดใน <iframe> มักจะมีลักษณะการทำงานแบบเบื้องหลังและหมดเวลาตามที่อธิบายไว้ข้างต้น

อีกความแตกต่างที่เกิดจากการแยกเว็บไซต์คือการจัดลําดับการเรียกใช้การยกเลิกโหลดแบบขนานใหม่ ดังนี้ หากไม่มีการแยกเว็บไซต์ ตัวแฮนเดิลการยกเลิกโหลดจะทํางานตามลําดับจากบนลงล่างอย่างเคร่งครัดในเฟรมต่างๆ แต่หากใช้การแยกเว็บไซต์ แฮนเดิลการยกเลิกการโหลดจะทํางานพร้อมกันในกระบวนการต่างๆ

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

กรณีสําคัญสําหรับตัวแฮนเดิลการยกเลิกการโหลดคือการส่งคําสั่ง ping สิ้นสุดเซสชัน ซึ่งมักจะทำดังต่อไปนี้

addEventListener('pagehide', () => {
  const image = new Image();
  img.src = '/end-of-session';
});

วิธีการที่ดีกว่าซึ่งมีประสิทธิภาพมากกว่าเมื่อเทียบกับการเปลี่ยนแปลงนี้คือการใช้ navigator.sendBeacon แทน ดังนี้

addEventListener('pagehide', () => {
  navigator.sendBeacon('/end-of-session');
});

หากต้องการควบคุมคำขอได้มากขึ้น คุณสามารถใช้ตัวเลือก keepalive ของ Fetch API ดังนี้

addEventListener('pagehide', () => {
  fetch('/end-of-session', {keepalive: true});
});

บทสรุป

การแยกเว็บไซต์ทำให้เว็บไซต์ที่ไม่น่าเชื่อถือเข้าถึงหรือขโมยข้อมูลจากบัญชีของคุณในเว็บไซต์อื่นๆ ได้ยากขึ้นด้วยการแยกแต่ละเว็บไซต์ออกเป็นกระบวนการของตนเอง ด้วยเหตุนี้ CORB จะพยายามทำให้ทรัพยากรข้อมูลที่ละเอียดอ่อนออกจากกระบวนการแสดงผล คำแนะนำข้างต้นจะช่วยให้คุณได้รับประโยชน์สูงสุดจากฟีเจอร์ความปลอดภัยใหม่เหล่านี้

ขอขอบคุณ Alex Moshchuk, Charlie Reis, Jason Miller, Nasko Oskov, Philip Walton, Shubhie Panicker และ Thomas Steiner สำหรับการอ่านบทความฉบับนี้และแสดงความคิดเห็น