ขโมย GitHub token ได้ใน 1 คลิกผ่านบั๊กของ VSCode
(blog.ammaraskar.com)- github.dev ใช้ OAuth token ที่ส่งต่อมาจาก github.com เพื่อเปิดดูไฟล์, ทำ PR และคอมมิตใน VSCode บนเบราว์เซอร์ โดย token นี้ไม่ได้ถูกจำกัดไว้เฉพาะรีโพซิทอรีใดรีโพซิทอรีหนึ่ง จึงสามารถอ่านและเขียนได้กับทุกรีโพซิทอรีที่ผู้ใช้เข้าถึงได้
- VSCode webview ถูกแยกด้วย iframe
vscode-webview://...แต่เพื่อรองรับ UX ของคีย์ลัด จึงมีการส่งkeydownจาก webview ไปยังหน้าต่างหลักเป็นข้อความdid-keydownทำให้ สคริปต์ที่ไม่น่าเชื่อถือ สามารถส่งอีเวนต์ให้เหมือนเป็นการกดแป้นพิมพ์ของผู้ใช้ได้ - การป้อนข้อความตามอำเภอใจใช้ไม่ได้เพราะ HTML
<input>แต่สามารถผสมคีย์ลัดค่าเริ่มต้นCtrl+Shift+A, การแจ้งเตือนติดตั้งส่วนขยายที่แนะนำ, local workspace extensions และคีย์ไบน์ดิงแบบกำหนดเอง เพื่อรันคำสั่งติดตั้งส่วนขยายได้ - PoC รัน JavaScript จากเซลล์ Markdown ของ Jupyter notebook เพื่อยอมรับการติดตั้งส่วนขยายที่แนะนำ ติดตั้งส่วนขยายที่เลือกผ่านคีย์ไบน์ดิงใหม่ จากนั้นแสดง GitHub API token และรายการรีโพซิทอรีส่วนตัว
- VSCode บนเดสก์ท็อปก็มีช่องโหว่เดียวกัน แต่ผู้โจมตีต้องหลอกให้เหยื่อโคลนรีโพซิทอรีและเปิด notebook ส่วนผู้ใช้ github.dev ควรล้างข้อมูลไซต์เพื่อให้กล่องยืนยันเริ่มต้นแสดงขึ้นมาอีกครั้งเป็นมาตรการป้องกัน
ภาพรวมของช่องโหว่
- github.dev จะเปิด VSCode แบบเบาที่รันในเบราว์เซอร์ เมื่อเปลี่ยน URL ของรีโพซิทอรี GitHub ที่เข้าถึงได้จาก
github.comเป็นgithub.devหรือคลิกเมนูที่เกี่ยวข้อง - VSCode บนเบราว์เซอร์นี้สามารถดูไฟล์ในรีโพซิทอรี เปิดรีโพซิทอรีส่วนตัว ส่ง PR และสร้างคอมมิตได้
- github.com จะส่ง OAuth token ไปยัง github.dev ผ่าน POST เพื่อใช้โต้ตอบกับ GitHub ในนามของผู้ใช้ และ token นี้ไม่ได้จำกัดอยู่กับรีโพซิทอรีเฉพาะที่ผู้ใช้กำลังใช้งาน
- ผู้โจมตีสามารถขโมย GitHub token ที่มีสิทธิ์อ่านและเขียนได้เพียงแค่ให้เหยื่อคลิกลิงก์ และเป้าหมายรวมถึงรีโพซิทอรีส่วนตัวด้วย
ปัญหาของการแยก Webview และการส่งต่อการกดแป้นพิมพ์
- VSCode webviews ใช้
<iframe>ที่มี origin ต่างจากหน้าต่างหลักของ VSCode เพื่อแยกการรัน JavaScript ออกจากกัน - เอาต์พุตของ Jupyter notebook ถูกเรนเดอร์ใน
<iframe>ที่มี originvscode-webview://...ขณะที่หน้าต่าง Electron หลักใช้ originvscode-file://... - การแยกนี้ทำให้แม้ notebook จะใช้การแสดงผล HTML หรือวิดเจ็ตโต้ตอบที่อิง JavaScript ก็ยังไม่สามารถเรียก Electron Node.js API หรือ VSCode API จากใน iframe ได้
- ฟีเจอร์ที่ต้องให้หน้าต่างหลักและ webview ทำงานร่วมกัน เช่น Markdown preview จะสื่อสารกันผ่าน Window.postMessage() API
- VSCode ส่งต่ออีเวนต์
did-keydownไปยังหน้าต่างหลัก เพื่อให้คีย์ลัดอย่างCtrl+Shift+Pยังทำงานได้แม้โฟกัสจะอยู่ใน webview - สคริปต์ที่ไม่น่าเชื่อถือภายใน webview สามารถสร้างอีเวนต์
keydownเองโดยตรง เพื่อปลอมเป็นการกดปุ่มจากผู้ใช้ได้
ห่วงโซ่การโจมตี
- แม้จะเปิด Command Palette ได้ด้วย
Ctrl+Shift+Pแต่การป้อนสตริงตามต้องการใช้ไม่ได้ เพราะ Command Palette ใช้ HTML<input> - อย่างไรก็ตาม อินพุตที่ประมวลผลผ่าน
keydownเช่น ปุ่มลูกศรและEnterยังใช้งานได้ และยังอาศัยชุดคีย์ลัดเริ่มต้นของ VSCode ได้ด้วย Ctrl+Shift+Aคือคีย์ไบน์ดิงเริ่มต้นของ “Notifications: Accept Notification Primary Action” ซึ่งจะกดปุ่มหลักของการแจ้งเตือนล่าสุดใน VSCode- หากใส่ส่วนขยายที่แนะนำไว้ใน
.vscode/extensions.jsonVSCode จะแสดงการแจ้งเตือนให้ติดตั้ง แต่ระบบ publisher trust ใน VSCode 1.97 จะเปิดกล่องความเชื่อถือแยกต่างหากเมื่อจะติดตั้งส่วนขยายจาก publisher ใหม่ - แม้จะย้ายโฟกัสปุ่มได้ด้วย
Tabแต่การจัดการEnterของปุ่ม “Trust Publisher & Install” ผูกอยู่กับkeydownของปุ่มนั้นเอง ทำให้เส้นทางนี้ติดตั้งให้เสร็จได้ยาก - local workspace extensions เปิดทางให้ติดตั้งส่วนขยายที่อยู่ใน
.vscode/extensionsได้โดยตรงภายใน workspace ที่เชื่อถือได้ และ github.dev/web workspace ถูกมองว่าเป็นสถานะเชื่อถืออยู่เสมอ - หากพยายามรัน local workspace extension โดยตรง extension worker จะคาดหวังส่วนขยายที่มาจาก
vscode-cdn.netทำให้เกิดข้อผิดพลาด CSP - ทางแก้คือเพิ่มคีย์ไบน์ดิงแบบกำหนดเองใน
package.jsonของ local workspace extension แล้วให้คีย์ไบน์ดิงนั้นเรียกworkbench.extensions.installExtensionด้วยคอนเท็กซ์skipPublisherTrust
การทำงานของ PoC และผลกระทบ
- องค์ประกอบที่ต้องมีคือรีโพซิทอรีที่มีทั้ง Jupyter notebook และ local workspace extension
- เซลล์ Markdown ของ notebook สามารถรัน JavaScript ได้ผ่านแอตทริบิวต์
onerrorของรูปภาพ - เพย์โหลดจะรอจน VSCode แสดงการแจ้งเตือนให้ติดตั้งส่วนขยายที่แนะนำ แล้วจึงส่งอีเวนต์
Ctrl+Shift+Aเพื่อยอมรับการกระทำหลักของการแจ้งเตือน - จากนั้นจะรอจนส่วนขยายถูกติดตั้งและเปิดใช้งาน พร้อมทั้งคีย์ไบน์ดิงแบบกำหนดเองพร้อมใช้งาน แล้วจึงส่งอีเวนต์
Ctrl+F1เพื่อทริกเกอร์การติดตั้งส่วนขยายที่เลือก - ส่วนขยายที่ติดตั้งผ่าน PoC จะดึง GitHub API token แล้วเรียก
https://api.github.com/user/reposเพื่อรับรายชื่อรีโพซิทอรีส่วนตัวที่เข้าถึงได้ ก่อนจะแสดง token และรายการรีโพซิทอรีในกล่องข้อมูล - หลังรัน PoC แล้วต้องล้างข้อมูลของ github.dev หรือลบส่วนขยาย PoC ออก ไม่เช่นนั้นมันจะติดตามไปทุกหน้า github.dev
- VSCode บนเดสก์ท็อปก็ได้รับผลจากช่องโหว่เดียวกัน แต่ผู้โจมตีต้องชักจูงให้เหยื่อโคลนรีโพซิทอรีและเปิด notebook ที่มีเพย์โหลดสคริปต์ webview
- หาก webview ที่เหยื่อเปิดมี XSS อื่นอยู่ด้วย บนเดสก์ท็อปก็อาจยกระดับกลายเป็น remote code execution เต็มรูปแบบได้ในทางปฏิบัติ
การป้องกันและปัจจัยบรรเทา
- หากไม่เคยใช้ github.dev มาก่อน จะมีกล่องโต้ตอบที่ต้องคลิกเมื่อเข้าเว็บไซต์ ซึ่งเป็นโอกาสให้หลุดออกจากหน้าการโจมตีได้
- หากล้างคุกกี้และข้อมูลไซต์ภายในเครื่องของ github.dev กล่องโต้ตอบเริ่มต้นนี้อาจแสดงขึ้นมาอีกครั้ง
- ใน Chrome สามารถกดไอคอนที่แถบ URL แล้วไปที่ Cookies and site data > Manage on-device site data เพื่อลบข้อมูลโดเมนที่เกี่ยวข้องได้
- หากเคยผ่านกล่องโต้ตอบของ github.dev มาแล้ว และยังไม่ได้ล้าง local storage ของเบราว์เซอร์ github.dev จะไม่มีตัวป้องกันอย่าง CSRF token ทำให้ลิงก์ใดๆ บนอินเทอร์เน็ตสามารถรีไดเร็กต์เข้าสู่การโจมตีได้
- VSCode ไม่ได้พึ่งพาแค่การแยก iframe เท่านั้น แต่ยังใช้ Content Security Policy ที่เข้มงวดและ DOMPurify ร่วมกัน
- ใน Markdown preview ของหน้าส่วนขยาย มีการใช้
script-src 'none'เพื่อกันการรัน JavaScript ตามอำเภอใจ จึงปิดกั้นผลกระทบที่ใหญ่กว่านั้นอย่าง desktop 1-click RCE ผ่านลิงก์ส่วนขยายเพียงอย่างเดียว
ที่มาของการเปิดเผยและไทม์ไลน์
- ก่อนหน้านี้ MSRC เคยแก้รายงานบั๊ก VSCode แบบเงียบๆ โดยไม่ให้เครดิต และระบุว่าไม่มีผลกระทบด้านความปลอดภัย
- รายงานบั๊ก VSCode XSS ล่าสุดของ Starlabs ก็ถูกจัดว่าไม่มีสิทธิ์และมีความรุนแรงต่ำ
- อาจเป็นไปได้ว่าทีม VSCode ยังต้องการเวลาเพิ่มเพื่อหาสมดุลระหว่าง UI/UX กับความปลอดภัย แต่ก็มีการเลือกเปิดเผยทั้งหมดด้วยเหตุผลว่าไม่ควรมองเวลาและความพยายามของนักวิจัยความปลอดภัยเป็นเรื่องแน่นอนอยู่แล้ว
- ก่อนเผยแพร่ในวันที่ 2 มิถุนายน 2026 หนึ่งชั่วโมง ได้มีการแจ้งกำหนดการเปิดเผยไปยังผู้ติดต่อเดิมของทีมความปลอดภัย GitHub
- ในวันเดียวกัน ช่องโหว่นี้ถูกเปิดเผยต่อสาธารณะและถูกลงทะเบียนไว้ใน VSCode issue tracker ด้วย
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
เป็นการสรุปที่ดี และในภาพใหญ่ก็น่าเสียดายที่ตัวแก้ไข VSCode ที่ฝังอยู่บนเว็บล็อกอินเข้า GitHub อยู่ตั้งแต่แรก
ไม่ว่าจะมี defense in depth หรือไม่ บาปดั้งเดิมข้อนั้นก็ทำให้พื้นผิวการโจมตีเพิ่มขึ้นมาก
มันคล้ายกับการวาง GitHub API token ที่มีสิทธิ์ทุกอย่างไว้เป็น plain text บนเวิร์กสเตชัน เพื่อให้แพ็กเกจ NPM อันตรายหาเจอได้
ในอุดมคติ IDE บนเบราว์เซอร์ควรรันด้วย ขอบเขตสิทธิ์ชั่วคราวแบบต่อรีโพซิทอรี หรือโทเค็นที่ทำได้แค่ pull/push กับรีโพนั้น และไม่ควรมีเว็บเซสชันของ github.com อยู่เลย
ถ้าต้องใช้ GitHub เว็บ UI แบบเต็มก็ค่อยกลับไปที่ github.com และให้ github.dev เป็นบริการสำหรับรีโพเดียว น่าจะเหมาะกว่า
แต่แน่นอนว่ามันทำให้ผู้ใช้ไม่สะดวก, ทำยาก, และอาจขัดกับสมมติฐานที่ฝังอยู่ในเครื่องมือ github.dev มานานแล้ว
github.dev ก็ควรพิจารณาแนวทางนี้อย่างจริงจัง
[1] https://orca.security/resources/blog/hacking-github-codespac...
ที่แย่กว่าคือดูเหมือนแม้แต่นักพัฒนาเองก็ไม่ได้ใส่ใจกันมากนัก
keydownใด ๆบนเดสก์ท็อปน่าจะดีกว่าถ้าให้ Electron ดักเองโดยตรงและตัดฟีเจอร์นี้ออกไป ส่วนบนเว็บก็ควรปิดไว้เป็นค่าเริ่มต้น
ไม่แน่ใจว่าโฮสต์ Git อื่นมีฟีเจอร์คล้ายกันหรือเปล่า
พูดตามตรง LLM agent ก็ควรทำแบบนี้เหมือนกัน การปล่อยให้ LLM push เองโดยตรงดูหุนหันเกินไป
เหตุผลที่การโจมตีนี้รับมือยากเป็นพิเศษคือ ส่วนขยาย VSCode รันด้วยระดับความเชื่อถือเดียวกับตัวแก้ไขเอง และนักพัฒนาส่วนใหญ่ก็ติดตั้งส่วนขยายกันเป็นสิบ ๆ ตัวโดยไม่เคยตรวจสิทธิ์เลย
ถ้าส่วนขยายเป็นอันตรายหรือถูกยึดไปแล้วแอบส่ง GitHub token ออกไป ก็แทบตรวจไม่พบหากไม่มีการเฝ้าดูเครือข่าย และนี่เป็นเหตุผลว่าทำไมส่วนขยายควรรันในโปรไฟล์ที่แยกกัน
วิธีที่ดีที่สุดคือย้ายออกจาก GitHub ไปใช้ GitLab/Forgejo ภายในองค์กร ที่โฮสต์เอง แล้วบล็อก GitHub ไปเลยทั้งหมด
ไม่นานมานี้ผมก็เจอเรื่องคล้ายกัน GitHub token กับ Cloudflare token ถูกขโมยไป
ต่อให้จริงจังกับความปลอดภัยแค่ไหน ถ้าเวลานานพอ สุดท้ายก็โดนกันได้อยู่ดี สิ่งที่ดีที่สุดคือแยกส่วนและควบคุมขอบเขตความเสียหาย
อย่าไว้ใจใครหรืออะไรทั้งนั้น ใช้ OrbStack และทำงานโดยตั้งสมมติฐานไว้เสมอว่าโทเค็นจะรั่วเข้าสักวัน
เวิร์กโฟลว์ผมพังไปหมด แต่ยังดีที่ฝั่งที่เอาโทเค็นไปดูเหมือนจะใกล้เคียงสแปมบอตมากกว่า พวกนั้นสร้างหน้าสแปมปลอมขึ้นมาเยอะมากและพยายามขุดคริปโต
ความรู้สึกที่ติดค้างที่สุดคือความรู้สึกว่าถูกละเมิด ขอให้ทุกคนระวังตัว
ตอนที่ส่งรายงานบั๊ก VSCode ไปยัง MSRC แล้วโดนแก้เงียบ ๆ โดยไม่มีอะไร เป็นประสบการณ์ที่แย่มาก ซึ่งก็เป็นสไตล์ MSRC ตามแบบฉบับอยู่แล้ว ดูเหมือนพวกเขาจะรู้แล้วว่านักวิจัยยังไงก็ส่งรายงานฟรีอยู่ดี เลยไม่เห็นเหตุผลต้องเปลี่ยนอะไร
ผมไม่รู้รายละเอียดของเคสนี้ แต่ก่อนหน้านี้เคยดูแลโปรแกรม bug bounty ผ่าน Bountysource และ HackerOne บางครั้งรายงานจะหลุดไปถึงทีมพัฒนาก่อนที่ทีมความปลอดภัยจะประเมินเสร็จ
พอถึงจุดนั้นนักพัฒนาอาจแก้เงียบ ๆ ไปเลยก็ได้ บางครั้งเพราะกังวลว่า ถ้ามันโยงกับบั๊กด้านความปลอดภัยจะทำให้ตัวเองดูไม่ดีหรือกระทบโอกาสเลื่อนขั้น ไม่ว่าจะสมเหตุสมผลหรือไม่ก็ตาม ผลคือพอทีมความปลอดภัยจะมาลองทำซ้ำ ช่องโหว่นั้นก็หายไปแล้ว
จากมุมของ MSRC พวกเขาเห็นแค่ว่าขั้นตอนทำซ้ำที่ให้มาใช้ไม่ได้แล้ว และมองไม่เห็นประวัติบั๊กภายในหรือว่ามีใครแพตช์ไปก่อนแล้วหรือไม่ ดังนั้นรายงานจึงถูกปิดว่าใช้ไม่ได้ แม้การค้นพบตั้งแต่แรกจะชอบธรรมก็ตาม
ขอบคุณที่แทบจะบริจาคเวลาที่ใช้กับเอ็กซ์พลอยต์นี้เพื่อชี้ให้เห็นว่าการตอบสนองด้านความปลอดภัยของ VS Code ยังต้องปรับปรุง แทนที่จะยอมแพ้ไปเฉย ๆ คุณก็ยังช่วยอยู่
ยังไม่ค่อยเข้าใจว่าทำไมนักพัฒนาถึงไม่ลอง Neovim กันมากกว่านี้
อาจเป็นเรื่องความชอบก็ได้ แต่ฉันชอบคอนฟิกเล็ก ๆ ที่พอมองออกว่าอะไรถูกติดตั้งไว้และอะไรกำลังรันอยู่ พอมีทั้ง VSCode, browser IDE, ส่วนขยาย, การซิงก์, โทเค็น และปลั๊กอินสุ่ม ๆ ปนกันไปหมด ก็ยิ่งยากที่จะรู้ว่าอะไรเข้าถึงอะไรได้บ้าง
นั่นเป็นฟีเจอร์ของส่วนขยาย Python อย่างเป็นทางการของ Microsoft และในหลายด้านมันก็แทบเป็นส่วนขยายเดียวที่พอใช้งานได้ แต่กลับไปติดตั้ง type definition ของไลบรารีคนละเวอร์ชันกับที่โปรเจ็กต์ของฉันใช้อยู่ ฉันรู้สึกไม่สบายใจมาก เพราะมันดูเหมือนรันโค้ดจากบุคคลที่สามที่ยังไม่ผ่านการตรวจสอบแบบไม่คิดอะไรเลย และก็ดูเหมือนจะไม่มีตัวเลือกให้ปิดด้วย
ฉันอยากจะพูดว่า “หลังจากนั้นก็ไม่เคยหันกลับไปมองอีกเลย” แต่พูดตรง ๆ คือช่วง 1~2 ปีที่ผ่านมา Neovim เริ่มทำคอนฟิกของฉันพังเป็นประจำแทบทุกครั้งที่อัปเกรด ก็พอจะมีลางอยู่แล้วว่าสักวันมันคงเป็นแบบนี้ ถ้าจะพูดกันให้เป๊ะ แม้จะผ่านไป 10 ปีแล้ว nvim ก็ยังไม่ได้ออก stable version แรกด้วยซ้ำ ดังนั้นจะไปโทษเรื่องความไม่เสถียรก็คงไม่ได้ แต่เป็นเรื่องที่ควรจำไว้
ตอนนี้กำลังคิดอยู่ว่าจะกลับไปใช้ Vim แบบเพียว ๆ ดีไหม ถึงจะเสียฟีเจอร์อำนวยความสะดวกไปเยอะ แต่ก็คงอยากลดการต้องมานั่งดีบักฟีเจอร์ที่พังระหว่างทำงาน
ไม่ต้องลงปลั๊กอินกองโตหรือใช้พวก SpaceVim ด้วย ลองดูสักครั้ง อาจจะชอบก็ได้
การชินกับ nvim ต้องใช้เวลา แต่พอชินแล้วมันเร็วกว่า ถึงอย่างนั้นก็อธิบายได้ว่าทำไมหลายคนถึงอยู่แต่ใน comfort zone
การ เปิดเผยต่อสาธารณะ เป็นเรื่องที่ทำได้ดีแล้ว คนที่ไม่พอใจกับ MSRC มีมากเกินไป และตอนนี้ก็เริ่มเดือดล้นเหมือนกรณี Nightmare Eclipse
ถ้ามีการเปิดเผยแบบนี้สะสมมากขึ้นเรื่อย ๆ บางที MSRC อาจหันกลับมาทบทวนตัวเองและตระหนักว่าปัญหาคือตัวพวกเขาเอง โอกาสดูจะน้อย แต่ก็หวังได้
ถึงอย่างนั้นอย่างน้อยก็น่าจะลองส่งดูก่อน หรืออย่างน้อยก็แจ้งล่วงหน้าหลายวันก่อนเปิดเผยต่อสาธารณะ เพราะไม่มีใครรู้หรอกว่าผลจะออกมาเป็นยังไง
บทความดีมาก แต่ช่วงท้ายฉันสับสนนิดหน่อย อยากเช็กว่าที่เข้าใจถูกไหม
ผู้เขียนบอกว่าด้วยระบบความน่าเชื่อถือของผู้เผยแพร่แบบใหม่ จะไม่สามารถติดตั้งส่วนขยายอันตรายได้โดยตรงด้วยแค่ทริกคีย์ลัด และถึงจะอ้อมไปใช้ local workspace extension ที่ไม่มีการตรวจสอบผู้เผยแพร่ได้ แต่ก็จะติด CSP
วิธีแก้ดูเหมือนจะเป็นการติดตั้ง local workspace extension ที่ bind คีย์ลัดสำหรับ “ติดตั้งส่วนขยายโดยไม่ตรวจสอบผู้เผยแพร่”
เลยสงสัยว่า 1) นี่หมายความว่าต้องใช้ส่วนขยายสองตัวใช่ไหม ตัวแรกเป็น local extension ที่มีหน้าที่แค่ key binding ส่วนตัวที่สองคือส่วนขยายอันตรายจริง ๆ ซึ่งเพราะติด CSP เลยไม่จำเป็นต้องเป็น local และจริง ๆ ก็เป็น local ไม่ได้ด้วยใช่ไหม และ 2) CSP บล็อกแค่ JS ของ local extension แต่ไม่ได้บล็อก
package.jsonหรือความสามารถในการเพิ่มคีย์ลัดใช่ไหมถ้าจะทำแบบตรงที่สุดก็คือใส่
my-extension/extension.jsลงไปได้เลย แต่ CSP จะบล็อกไว้ อย่างไรก็ตามscript-srcCSP บล็อกเฉพาะสคริปต์ ดังนั้นการโหลดpackage.jsonยังทำได้ เลยอาศัยช่องนั้นเพื่อเพิ่ม key binding ได้สถานการณ์ของ MSRC นี่เหลือเชื่อจริง ๆ
อาจจะมีแหล่งข้อมูลที่ดีกว่านี้ แต่ฉันคิดว่าวิดีโอนี้ของ The Primeagen เป็นจุดเริ่มต้นที่ดี
https://www.youtube.com/watch?v=9kxx5xp5nTQ
ตรงที่บอกว่า “วิธีเดียวที่จะยอมให้พฤติกรรมนี้เกิดขึ้นได้คือให้เว็บเพจสองหน้าที่มาจากคนละ origin ร่วมมือกันผ่าน API
Window.postMessage()” ฉันมีข้อติงเล็กน้อยจริง ๆ แล้วยังสื่อสารกันได้ผ่านการที่ iframe หรือหน้าต่างแม่เปลี่ยนพร็อพเพอร์ตี
location.anchorด้วย