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 ประเภท ได้แก่
- แหล่งข้อมูล เช่น เอกสาร HTML, XML หรือ JSON
- ทรัพยากรสื่อ เช่น รูปภาพ, 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 ที่อ่านฉบับร่างของบทความนี้และให้ความคิดเห็น