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

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

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

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

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

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

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

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

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

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

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

ในสถานการณ์นี้ เครื่องจัดการ unload ในเฟรมทั้งหมดเชื่อถือได้มาก

อย่างไรก็ตาม แม้จะไม่มีการแยกเว็บไซต์ การนำทางเฟรมหลักบางส่วนจะเป็นแบบข้ามกระบวนการ ซึ่งจะส่งผลต่อการทำงานของตัวแฮนเดิลการยกเลิกการโหลด ตัวอย่างเช่น หากพิมพ์ URL จาก old.example ไปยัง new.example ในแถบที่อยู่ การไปยังส่วนต่างๆ ของ 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 สำหรับการอ่านฉบับร่างของบทความนี้และแสดงความคิดเห็น