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

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

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

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

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

การบล็อกการอ่านข้ามแหล่งที่มาหรือ 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>

เมื่อใดก็ตามที่วิดเจ็ตโซเชียลได้รับข้อความผ่าน postMessage API ระบบจะบันทึกความกว้างขององค์ประกอบ <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 ที่อ่านฉบับร่างของบทความนี้และให้ความคิดเห็น