Chrome 142+ ทำให้เวิร์กโฟลว์สคริปต์ในเครื่องพังได้อย่างไร และเราแก้ไขอย่างไร

David Négrier
CTO & Founder
เทคนิค

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

หากคุณใช้ map starter kit ของ WorkAdventure (ภาษาอังกฤษ) แล้วสคริปต์ที่คุณเขียนเองหยุดโหลดจาก localhost ไปเฉย ๆ คุณไม่ได้ตั้งค่าอะไรผิด Chrome เปลี่ยนวิธีจัดการคำขอภายในเครื่องที่มาจาก iframe แบบ sandbox และวิธีเดิมของเราก็พึ่งพาสิ่งนั้นพอดี

ในบทความนี้ ผมจะอธิบายว่าอะไรเปลี่ยนไป ทำไมจึงล้มเหลว (แม้จะตั้ง header CORS แบบอนุญาตกว้างแล้วก็ตาม) วิธีแก้ขัดระยะสั้นที่เราปล่อยออกไป และวิธีแก้ระยะยาวที่ทำให้สภาพแวดล้อมการพัฒนาและ production ทำงานเหมือนกัน

หากคุณต้องการแค่วิธีแก้

วิธีแก้ถูกปล่อยแล้วใน WorkAdventure v1.27.6 วิธีแก้นี้เปิดความเป็นไปได้ใหม่ ๆ ให้สคริปต์ในเครื่อง และหากคุณต้องการได้ประโยชน์จากความสามารถเหล่านี้เมื่อสคริปต์ของคุณขึ้น production แล้ว เราแนะนำให้คุณ:

อัปเดต plugin ของ Vite ในไฟล์ package.json ของ map starter kit:

"wa-map-optimizer-vite": "^1.2"

แล้วรัน npm install อีกครั้ง

ตอนนี้สคริปต์ของคุณจะโหลดจาก origin ที่ถูกต้องทั้งในการพัฒนาและใน production ซึ่งทำให้พฤติกรรมของ CORS คาดเดาได้ง่ายขึ้นมากด้วย

ส่วนที่เหลือของบทความอธิบายว่าเกิดอะไรขึ้น และทำไมวิธีนี้จึงแก้ปัญหาได้

อะไรเปลี่ยนไปใน Chrome 142

Chrome 142 มีการเปลี่ยนแปลงสองอย่างที่เกี่ยวข้องกับเรื่องนี้

1) หน้าต่างขออนุญาต “การเข้าถึงเครือข่ายภายใน” แบบใหม่

เมื่อหน้าเว็บพยายามเข้าถึงทรัพยากรบนเครือข่ายภายใน เช่น localhost ตอนนี้ Chrome กำหนดให้ผู้ใช้ต้องตัดสินใจอย่างชัดเจน (ภาษาอังกฤษ)

ในกรณีของเรา เมื่อ WorkAdventure (โฮสต์อยู่ที่ https://play.workadventu.re) พยายามโหลดทรัพยากรจากเซิร์ฟเวอร์พัฒนาในเครื่องของคุณที่ localhost:5173 Chrome จะแสดงหน้าต่างถามว่าจะอนุญาตหรือปฏิเสธการเข้าถึงเครือข่ายภายใน

หน้าต่างขออนุญาตเข้าถึงเครือข่ายภายในของ Chrome สำหรับคำขอไปยัง localhost (อนุญาตหรือปฏิเสธ)

ส่วนนี้ยังพอรับมือได้ คุณแค่คลิก “อนุญาต” แล้วทำงานต่อ

2) การเปลี่ยนแปลงที่ทำให้พัง: คำขอภายในเครื่องจาก opaque origin ถูกบล็อก

การเปลี่ยนแปลงที่กระทบหนักกว่าคือวิธีที่ Chrome จัดการคำขอไปยังเครือข่ายภายในที่มาจาก origin ที่มันมองว่าไม่น่าเชื่อถือ

iframe แบบ sandbox ที่สร้างโดยไม่มี allow-same-origin จะได้ origin แบบทึบ (opaque origin) ในทางปฏิบัติ มักแสดงเป็น origin: null ใน Chrome 142 คำขอไปยังเครือข่ายภายในที่มาจาก null จะถูกบล็อกตั้งแต่เนิ่น ๆ ก่อนที่ CORS จะถูกตรวจสอบเสียอีก

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

ทำไมเวิร์กโฟลว์ของ map starter kit จึงพัง

map starter kit ของ WorkAdventure (ภาษาอังกฤษ) ออกแบบมาเพื่อให้ปรับแก้และทดลองได้รวดเร็ว

เมื่อคุณรัน:

$ npm run dev

เซิร์ฟเวอร์พัฒนาจะเริ่มทำงานที่ localhost:5173 จากนั้น WorkAdventure จะโหลดแผนที่ รูปภาพ JavaScript CSS และทรัพยากรอื่น ๆ จากเซิร์ฟเวอร์นั้น คุณจึงเห็นการเปลี่ยนแปลงแบบเรียลไทม์

สำหรับทรัพยากรของแผนที่ หน้าต่างขออนุญาตใหม่ของ Chrome มักเพียงพอให้ทำงานต่อได้ แต่สคริปต์ใน WorkAdventure เป็นกรณีพิเศษ

สคริปต์ทำงานใน iframe แบบ sandbox โดยการออกแบบ

WorkAdventure ให้คุณเขียนสคริปต์ของตัวเองเพื่อเพิ่มการโต้ตอบผ่าน scripting API ด้วยเหตุผลด้านความปลอดภัย สคริปต์ของผู้ใช้เหล่านี้จะไม่ถูกรันในบริบทหลักของ WorkAdventure แต่จะรันอยู่ภายใน iframe แบบ sandbox

ก่อน Chrome 142 ขั้นตอนการพัฒนาในเครื่องของเราเป็นแบบนี้:

  1. WorkAdventure สร้าง iframe เปล่าด้วย sandbox=“allow-scripts” (ไม่มี allow-same-origin)

  2. ใช้ srcdoc เพื่อแทรกเอกสาร HTML ขนาดเล็กลงใน iframe นั้น

  3. เอกสารนั้นมีแท็ก <script> ที่ชี้ไปยังเซิร์ฟเวอร์พัฒนาของคุณ เช่น http://localhost:5173/my-script.js

  4. เบราว์เซอร์ดึงสคริปต์จาก localhost แล้วรันภายใน iframe แบบ sandbox

แผนภาพลำดับขั้นตอน: Chrome 142+ บล็อกสคริปต์จาก localhost ที่โหลดใน iframe แบบ sandbox (origin null)

หลัง Chrome 142 ขั้นตอนที่ 4 ล้มเหลว

สิ่งที่คุณจะเห็นเมื่อเกิดข้อผิดพลาด

เมื่อสคริปต์ถูกบล็อก มันจะไม่ถูกโหลดเลย และ Chrome จะบันทึกข้อผิดพลาดแบบนี้:

Access to script at ‘http://localhost:5173/src/main.ts ’ from origin ‘null’ has been blocked by CORS policy: Permission was denied for this request to access the unknown address space.

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

วิธีแก้แรก: ปลดล็อกการพัฒนาในเครื่องด้วย endpoint แบบ proxy

เพื่อให้นักพัฒนากลับมาทำงานได้โดยเร็ว เราเปลี่ยนวิธีที่ WorkAdventure โหลดสคริปต์ของผู้ใช้ระหว่างการพัฒนาในเครื่อง

ตอนนี้เซิร์ฟเวอร์ WorkAdventure มี endpoint แบบ proxy:

/local-script?script=XXX

แทนที่จะปล่อยให้ iframe แบบ sandbox ดึงสคริปต์จาก localhost โดยตรง WorkAdventure จะดึงผ่าน endpoint นี้ ทำให้คำขอมาจาก origin ของเซิร์ฟเวอร์ WorkAdventure ไม่ใช่จาก null

“นี่ไม่ใช่ช่องโหว่ด้านความปลอดภัยหรือ?”

อาจเป็นได้ ขึ้นอยู่กับวิธีการพัฒนา เราจึงจำกัดขอบเขตไว้อย่างเข้มงวด

endpoint นี้อนุญาตเฉพาะสคริปต์ที่โฮสต์อยู่บน localhost เท่านั้น ดังนั้นแม้มันจะเปิดให้โหลดสคริปต์ในเครื่อง “ใดก็ได้” ผู้โจมตีก็ต้องเข้าถึงเครื่องของผู้ใช้ได้ก่อนจึงจะโฮสต์สิ่งที่เป็นอันตรายบน localhost ได้ ซึ่งเป็นด่านที่สูงกว่ามาก และหากผู้โจมตีเข้าถึงเครื่องได้แล้ว ระบบก็ถือว่าถูกเจาะไปแล้วอยู่ดี

วิธีนี้แก้ปัญหาเฉพาะหน้าที่ Chrome 142 ทำให้พังได้ แต่ก็สร้างปัญหาใหม่ขึ้นมา

ข้อเสียที่ซ่อนอยู่: การพัฒนาและ production ไม่ตรงกันอีกต่อไป

เมื่อใช้วิธี proxy การพัฒนาในเครื่องได้เปลี่ยน origin ที่แท้จริงของสคริปต์ไป

ในการพัฒนา สคริปต์ถูกโหลดจาก iframe ที่มี origin เป็น https://play.workadventu.re (เซิร์ฟเวอร์ WorkAdventure) แต่ใน production สคริปต์ยังถูกโหลดด้วย srcdoc origin ของ iframe จึงยังเป็น null

ความไม่ตรงกันนี้อาจทำให้เกิดปัญหาที่วินิจฉัยได้ยาก

หากสคริปต์ต้องพึ่งการรันภายใต้ origin จริงที่มีสิทธิ์มากกว่า เช่น การเข้าถึงคุกกี้ localStorage หรือทรัพยากรที่ผูกกับ origin ซึ่ง WorkAdventure ให้บริการ สคริปต์อาจทำงานได้ในเครื่อง แล้วพังหลัง deploy

เราจึงตัดสินใจไปให้ไกลกว่านั้น และแก้ความไม่สอดคล้องที่ต้นเหตุ

วิธีแก้ระยะยาว: ให้สคริปต์มี origin จริงในทุกที่

สิ่งที่เราต้องการเหนือสิ่งอื่นใดคือ สคริปต์ต้องโหลดแบบเดียวกันทั้งในการพัฒนาและใน production

การใช้ /local-script ใน production ไม่ใช่ทางเลือก เพราะ proxy ใน production ที่ส่งสคริปต์ใด ๆ ที่โฮสต์บน WorkAdventure ให้กับโค้ดของผู้ใช้ได้ จะเป็นความเสี่ยงด้านความปลอดภัยครั้งใหญ่ ซึ่งอาจเปิดทางให้เข้าถึง API ภายในและข้อมูลผู้ใช้

เราจึงอาศัยสิ่งที่เป็นจริงอยู่แล้วเกี่ยวกับแผนที่ของ WorkAdventure แทน

ปัจจุบันแผนที่โฮสต์อยู่ที่ไหน

แผนที่ของ WorkAdventure โฮสต์อยู่บน:

  • GitHub Pages สำหรับการตั้งค่าแบบเดิม
  • หรือเซิร์ฟเวอร์จัดเก็บแผนที่ (map storage) ของ WorkAdventure

สิ่งนี้ให้ทางออกที่เรียบร้อย: โฮสต์หน้า HTML ไว้คู่กับสคริปต์ แล้วโหลดหน้านั้นใน iframe ด้วย URL src ตามปกติ

ทำไมวิธีนี้จึงปลอดภัยเพียงพอ

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

ในการตั้งค่าแบบโฮสต์เอง เซิร์ฟเวอร์จัดเก็บแผนที่มักอยู่บนโดเมนเดียวกับ WorkAdventure หรืออยู่บนโดเมนเฉพาะที่ผู้ดูแลระบบคนเดียวกันควบคุม ในบริบทนี้ เจ้าของแผนที่และผู้ดูแลเซิร์ฟเวอร์มักเป็นคนเดียวกัน จึงไม่ได้สร้างความเสี่ยงใหม่ระหว่างผู้เช่า (tenant) ต่าง ๆ

สิ่งที่เราเปลี่ยนใน map starter kit

map starter kit มาพร้อม plugin ของ Vite ที่ทำหน้าที่ build สคริปต์ เราขยาย plugin นั้นให้สร้างหน้า HTML ขนาดเล็กสำหรับแต่ละสคริปต์ด้วย ซึ่งจะโหลดสคริปต์ผ่านแท็ก <script>

ตัวอย่างเช่น หากคุณมีสคริปต์ชื่อ my-script.js plugin จะสร้าง my-script.html แบบนี้:

<!DOCTYPE html>
<html lang="en">
<head>
    <script src="https://play.workadventu.re/iframe_api.js"></script>
    <script src="my-script.js"></script>
</head>
<body>
</body>
</html>

จากนั้น WorkAdventure จะโหลดหน้า HTML ใน iframe ด้วย src แทนการแทรก srcdoc แล้วพยายามดึงสคริปต์จาก localhost โดยตรง

ผลลัพธ์เป็นอย่างที่เราต้องการทุกประการ:

  • origin ที่ถูกต้องในการพัฒนา
  • origin ที่ถูกต้องใน production
  • พฤติกรรมที่สอดคล้องกันในทุกสภาพแวดล้อม

ผลพลอยได้คือ สคริปต์ที่เคยมีปัญหากับทรัพยากรที่ป้องกันด้วย CORS ตอนนี้เข้าใจและคาดเดาได้ง่ายขึ้นมาก

สรุป

Chrome 142 ไม่ได้แค่เพิ่มหน้าต่างขออนุญาต แต่ยังเปลี่ยนวิธีจัดการคำขอไปยังเครือข่ายภายในที่มาจาก opaque origin เช่น iframe แบบ sandbox ที่มี origin: null ด้วย ซึ่งทำให้รูปแบบที่ใช้กันทั่วไปในการโหลดสคริปต์ในเครื่องระหว่างพัฒนาพังลง

เราปล่อยวิธีแก้ขัดระยะสั้นเพื่อปลดล็อกการพัฒนาในเครื่อง จากนั้นจึงพัฒนาแนวทางที่แข็งแรงกว่า: โหลดสคริปต์ผ่านหน้า HTML เฉพาะที่โฮสต์ไว้คู่กับแผนที่ เพื่อให้สคริปต์มี origin จริงทั้งในการพัฒนาและใน production

หากคุณใช้ map starter kit การอัปเกรด wa-map-optimizer-vite เป็น ^1.2 จะทำให้เวิร์กโฟลว์ของคุณกลับมาราบรื่น และเจอเรื่องไม่คาดคิดเกี่ยวกับ origin และ CORS น้อยลง