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