- แพ็กเกจ npm อันตราย Shai-Hulud 2.0 ติดมัลแวร์ลงบนเครื่องของนักพัฒนาและขโมย สิทธิ์เข้าถึง GitHub Organization ของ Trigger.dev
- การติดเชื้อเริ่มขึ้นเมื่อผู้พัฒนารัน
pnpm install แล้ว สคริปต์ preinstall ของแพ็กเกจอันตรายถูกเรียกใช้ โดยใช้เครื่องมือ TruffleHog เพื่อขโมยข้อมูลรับรอง
- ผู้โจมตี โคลนรีโพ 669 แห่ง ภายใน 17 ชั่วโมง และจากนั้นในช่วง 10 นาทีได้พยายาม force push ไปยัง 199 สาขาและปิด PR 42 รายการ
- แพ็กเกจและระบบโปรดักชันไม่ได้รับความเสียหาย และตรวจพบการโจมตีภายใน 4 นาทีพร้อมตัดการเข้าถึงบัญชีได้ทันที
- หลังเหตุการณ์ มีการเสริมความปลอดภัยด้วยการปิดใช้งานสคริปต์ npm, อัปเกรดเป็น pnpm 10, ใช้ การเผยแพร่ npm แบบอิง OIDC, และ บังคับใช้ branch protection กับทุกรีโพ
ภาพรวมการโจมตี
- วันที่ 25 พฤศจิกายน 2025 ระหว่างการดีบักภายใน Slack พบความผิดปกติว่ามี คอมมิต “init” ในนาม Linus Torvalds ถูกสร้างขึ้นในหลายรีโพ
- จากการตรวจสอบพบว่าเวิร์มซัพพลายเชน Shai-Hulud 2.0 ได้ติดมัลแวร์ลงบนเครื่องของนักพัฒนาและขโมยข้อมูลรับรอง GitHub
- มีรายงานว่าเวิร์มนี้แพร่ไปยังแพ็กเกจ npm มากกว่า 500 รายการ และส่งผลกระทบต่อรีโพมากกว่า 25,000 แห่ง
- แพ็กเกจ npm ทางการของ Trigger.dev (
@trigger.dev/*, CLI) ไม่ได้รับการติดเชื้อ
ไทม์ไลน์การโจมตี
- 24 พฤศจิกายน 04:11 UTC: เริ่มเผยแพร่แพ็กเกจอันตราย
- 20:27 UTC: เครื่องของนักพัฒนาในเยอรมนีติดเชื้อ
- 22:36 UTC: ผู้โจมตีเข้าถึงได้เป็นครั้งแรกและเริ่มโคลนรีโพจำนวนมาก
- 15:27~15:37 UTC (25 พฤศจิกายน): ดำเนินการโจมตีเชิงทำลายเป็นเวลา 10 นาที
- 15:32 UTC: ตรวจพบความผิดปกติและตัดการเข้าถึงได้ภายใน 4 นาที
- 22:35 UTC: กู้คืนทุกสาขาเสร็จสมบูรณ์
กระบวนการติดเชื้อ
- เมื่อผู้พัฒนารัน
pnpm install สคริปต์ preinstall ของแพ็กเกจอันตรายจะถูกเรียกใช้ เพื่อดาวน์โหลดและรัน TruffleHog
- TruffleHog จะสแกนหา GitHub token, ข้อมูลรับรอง AWS, npm token, ตัวแปรแวดล้อม เป็นต้น แล้วส่งออกไปภายนอก
- พบไดเรกทอรี
.trufflehog-cache และไฟล์ที่เกี่ยวข้องบนเครื่องที่ติดเชื้อ
- แพ็กเกจต้นทางที่ทำให้เกิดการติดเชื้อถูกลบไปแล้ว จึงติดตามย้อนกลับไม่ได้
กิจกรรมของผู้โจมตี
- หลังการติดเชื้อ มีการทำ กิจกรรมสอดแนม ต่อเนื่องเป็นเวลา 17 ชั่วโมง
- ใช้อินฟราจากสหรัฐฯ และอินเดียเพื่อโคลนรีโพ 669 แห่ง
- เฝ้าติดตามกิจกรรมของนักพัฒนาและคงการเข้าถึงไว้ผ่าน GitHub token
- สร้างรีโพชื่อ “Sha1-Hulud: The Second Coming” ซึ่งคาดว่าใช้เก็บข้อมูลรับรอง
- จากนั้นในช่วง 10 นาทีได้ทำ การกระทำเชิงทำลาย
- พยายาม force push ไปยัง 199 สาขาใน 16 รีโพ
- ปิด PR 42 รายการ โดยบางส่วนถูกบล็อกด้วยการตั้งค่า branch protection
- ทุกคอมมิตแสดงในรูปแบบ “Linus Torvalds <email> / init”
การตรวจจับและการตอบสนอง
- ตรวจพบความผิดปกติแบบเรียลไทม์ผ่านการแจ้งเตือนใน Slack
- ภายใน 4 นาที ได้ตัดการเข้าถึง GitHub ของบัญชีที่ติดเชื้อ และหลังจากนั้นเพิกถอนการเข้าถึงบริการทั้งหมด เช่น AWS, Vercel, Cloudflare
- จากการวิเคราะห์ AWS CloudTrail log พบว่า มีเพียงการเรียก API แบบอ่านอย่างเดียว และไม่มีการเข้าถึงข้อมูลโปรดักชัน
- AWS ยังตรวจพบพฤติกรรมต้องสงสัยที่เกี่ยวข้องกับ Shai-Hulud แยกต่างหากและส่งคำเตือนมา
ความเสียหายและการกู้คืน
- มี รีโพถูกโคลน 669 แห่ง, force push ไปยัง 199 สาขา, และ PR ถูกปิด 42 รายการ
- แม้การกู้คืนจะยากเพราะ GitHub ไม่มี server-side reflog แต่สามารถใช้ Event API และ local reflog เพื่อกู้คืนทั้งหมดได้ภายใน 7 ชั่วโมง
- แพ็กเกจ npm และโครงสร้างพื้นฐานโปรดักชันไม่ได้รับความเสียหาย
การรั่วไหลของ GitHub App key
- ระหว่างการตรวจสอบ พบ GitHub App private key อยู่ในถังขยะของโน้ตบุ๊กนักพัฒนา
- คีย์ดังกล่าวมี สิทธิ์ read/write ต่อรีโพของลูกค้า และได้มีการ rotate ทันที
- ฐานข้อมูล (ที่เก็บ installation ID) ไม่ได้รับความเสียหาย จึง ไม่มีหลักฐานว่ามีการเข้าถึงรีโพของลูกค้า แต่ยังไม่อาจตัดความเป็นไปได้นี้ออกได้ทั้งหมด
- ได้ขอ log เพิ่มเติมจากทีมสนับสนุน GitHub และส่งอีเมลแจ้งลูกค้าแล้ว
การวิเคราะห์ทางเทคนิคของ Shai-Hulud
- เมื่อรัน
setup_bun.js จะติดตั้ง Bun runtime และรัน bun_environment.js แบบเบื้องหลัง
- ใช้ TruffleHog เพื่อรวบรวมข้อมูลรับรองภายในไดเรกทอรี
$HOME
- ข้อมูลที่รวบรวมได้ (
contents.json, cloud.json, truffleSecrets.json เป็นต้น) จะถูกอัปโหลดไปยัง รีโพ GitHub แบบสุ่มในรูปแบบเข้ารหัส base64 ซ้อน 3 ชั้น
- หากมี npm token อยู่ เวิร์มจะปรับแก้และเผยแพร่แพ็กเกจของบัญชีที่ติดเชื้อใหม่เพื่อขยายการแพร่กระจาย
- หากไม่พบข้อมูลรับรอง จะมีการ พยายามลบ home directory
- ไฟล์บ่งชี้การติดเชื้อ:
setup_bun.js, bun_environment.js, .trufflehog-cache/ เป็นต้น
มาตรการเสริมความปลอดภัย
- ปิดใช้งานสคริปต์ npm ทั้งหมด (
ignore-scripts=true)
- อัปเกรดเป็น pnpm 10: ปิดการรันสคริปต์โดยค่าเริ่มต้น และตั้งค่า
minimumReleaseAge (3 วัน) เพื่อหน่วงการติดตั้งแพ็กเกจใหม่
- นำ npm Trusted Publishers แบบอิง OIDC มาใช้เพื่อลบ long-lived token
- บังคับใช้ branch protection กับทุกรีโพ
- นำ Granted มาใช้กับ AWS SSO และเข้ารหัส session token
- เปลี่ยนให้ GitHub Actions ต้องได้รับการอนุมัติก่อนเมื่อรัน workflow ของผู้มีส่วนร่วมภายนอก
บทเรียนสำหรับทีมอื่น
- โครงสร้างที่เปิดให้มี การรันโค้ดตามอำเภอใจระหว่างการติดตั้ง npm คือพื้นผิวการโจมตีในตัวมันเอง
- ควรตั้งค่า
ignore-scripts=true และดูแล whitelist เฉพาะแพ็กเกจที่จำเป็น
- ใช้ pnpm minimumReleaseAge เพื่อหน่วงการติดตั้งแพ็กเกจใหม่
- branch protection และ การดีพลอยแบบอิง OIDC เป็นมาตรการความปลอดภัยที่จำเป็น
- ห้ามเก็บข้อมูลรับรองระยะยาวไว้บนเครื่องโลคัล และอนุญาตให้ดีพลอยผ่าน CI เท่านั้น
- สัญญาณรบกวนจากการแจ้งเตือนใน Slack กลับกลายเป็นกุญแจสำคัญของการตรวจจับ
มิติด้านมนุษย์
- นักพัฒนาที่เครื่องติดเชื้อไม่ได้ทำอะไรผิด เพราะเพียงแค่รัน
npm install ก็ได้รับผลกระทบแล้ว
- ระหว่างการโจมตี พบร่องรอยว่าบัญชีดังกล่าวไปกด ‘star’ รีโพแบบสุ่มหลายร้อยแห่งโดยอัตโนมัติ
- เหตุการณ์นี้ไม่ได้สะท้อนความผิดพลาดของบุคคล แต่เผยให้เห็น ความเปราะบางเชิงโครงสร้างของทั้ง ecosystem
ตัวชี้วัดสรุป
- ตั้งแต่ติดเชื้อครั้งแรกจนถึงการโจมตีครั้งแรก: ประมาณ 2 ชั่วโมง
- ระยะเวลาที่ผู้โจมตีคงการเข้าถึงไว้: 17 ชั่วโมง
- ระยะเวลาของการกระทำเชิงทำลาย: 10 นาที
- ใช้เวลา 5 นาทีจนตรวจพบ และ 4 นาทีจนตัดการเข้าถึง
- ใช้เวลา 7 ชั่วโมงจนกู้คืนทั้งหมดเสร็จสมบูรณ์
- รีโพที่ถูกโคลน: 669 แห่ง / สาขาที่ได้รับผลกระทบ: 199 / PR ที่ถูกปิด: 42
แหล่งข้อมูลอ้างอิง
- Socket.dev: Shai-Hulud Strikes Again V2
- รายงานวิเคราะห์จาก PostHog, Wiz, Endor Labs, HelixGuard
- เอกสารของ npm Trusted Publishers, pnpm
onlyBuiltDependencies, minimumReleaseAge, Granted
3 ความคิดเห็น
ดูเหมือนว่าโดยโครงสร้างแล้ว pnpm จะต้องอนุญาต
post-installเป็นรายตัวโดยค่าเริ่มต้นอยู่แล้ว แต่สุดท้ายนักพัฒนาก็อาจเผลออนุญาตไปโดยไม่รู้ตัวกันอยู่ดีเข้าใจว่าเนื่องจาก
npmถูกตั้งให้ทำงานเป็นค่าเริ่มต้น จึงเปลี่ยนไปใช้pnpmและปิดการทำงานค่าเริ่มต้นไว้เพื่อเสริมความปลอดภัยให้แข็งแกร่งขึ้นความคิดเห็นจาก Hacker News
การรัน
npm installไม่ใช่ความ ประมาทเลินเล่อปัญหาคือ ecosystem ที่ อนุญาตให้รันโค้ดตามอำเภอใจ ระหว่างขั้นตอนติดตั้งแพ็กเกจ
แต่ความล้มเหลวด้านความปลอดภัยที่แท้จริงคือการใช้ package manager ที่เปิดให้บุคคลที่สามยัดโค้ดเข้ามาในผลิตภัณฑ์ของฉันได้โดยแทบไม่มีการตรวจสอบ
สุดท้ายแล้วเรากำลังพึ่งพาความหวังดีและความสามารถของ package manager และผู้ดูแลมันอย่างไม่มีที่สิ้นสุด
อีกอย่าง ดูเหมือน OP จะสื่อว่าได้เก็บ credential ไว้ในระบบไฟล์แบบ plain text
ในระดับภาษา เราสามารถสร้างโครงสร้างที่จำกัดให้โค้ดอ่านอินพุต ใช้ทรัพยากร และ สร้างเอาต์พุตที่ถูกต้องตามชนิดเท่านั้น ได้
แม้จะไม่ใช่การแก้ปัญหา supply chain แบบสมบูรณ์ แต่ก็ลดขอบเขตความเสี่ยงลงได้มาก
ประมาณว่า “ไม่ผิดถ้าคนคนหนึ่งใช้เครื่องมือแบบนี้ แต่ถ้าทุกคนใช้ ecosystem จะมีปัญหา”
มันพิสูจน์มาหลายครั้งแล้วว่าเครื่องมือสำหรับนักพัฒนาจำนวนมากนั้น มีช่องโหว่ด้านความปลอดภัย
ถ้าใส่ใจจริง ก็ต้องแสดงออกผ่านการกระทำ
ตอนใช้ VS Code มันน่ารำคาญที่แค่จะเพิ่มฟังก์ชันเล็กน้อยก็ต้องติดตั้งปลั๊กอินจากคนที่ไม่รู้จักว่าเป็นใคร
ท้ายที่สุดมันก็เป็นโครงสร้างที่ต้องรัน โค้ดที่เลี่ยงไม่ได้ว่าจะต้องเชื่อถือ ไม่ใช่หรือ
netrcสำหรับการยืนยันตัวตนผ่าน httpถ้าใช้ git ผ่าน http เส้นทางการ เปิดเผย credential แบบ plain text ลักษณะนี้แทบจะมีอยู่เสมอ
ผู้ดูแล pnpm เคยเสนอไว้ตั้งแต่ปีก่อนว่าให้ “บล็อก post-install script โดยค่าเริ่มต้น”
แม้จะทำให้ฝั่งผู้ใช้ไม่สะดวก แต่เชื่อว่าในระยะยาวจะเป็นการเปลี่ยนแปลงที่ทุกคนจะขอบคุณ
PR ที่เกี่ยวข้อง: pnpm/pnpm#8897
สุดท้ายก็เป็นอีกครั้งที่ ความสะดวกชนะความปลอดภัย
เขาบอกว่า “ฐานข้อมูลไม่ได้ถูกเจาะ” แต่ถ้าผู้โจมตี เข้าถึง AWS และซีเคร็ตได้ ผมก็ถือว่ามันถูกเจาะไปแล้ว
ถ้ามีความเป็นไปได้ที่จะเข้าถึงได้ ก็ควรถูกนับว่าเป็นการถูกเจาะ
หลังจากมัลแวร์ถูกรันแล้ว แทบจะ เป็นไปไม่ได้เลยที่จะตามรอยต้นตอ
pnpm installก็เสร็จสมบูรณ์ตามปกติด้วย จึงตรวจจับได้ยากถ้ามี EDR อย่าง Sentinel One หรือ CrowdStrike ก็น่าจะมีเบาะแสสำหรับสืบสวนมากกว่านี้
ส่วนที่บอกว่า “clone repo ไปทั้งหมด 669 แห่ง” สะดุดตา
บริษัทที่มีพนักงานไม่ถึง 100 คนแต่มี repo มากกว่า 600 อันนี่ถือว่าปกติไหม
จำนวน repo ได้รับอิทธิพลจาก อายุองค์กรและอายุของโปรเจกต์ มากกว่าขนาดทีม
pnpm หยุด รัน lifecycle script อัตโนมัติ อย่าง
preinstallไปแล้ว ดูเหมือนพวกเขาจะใช้เวอร์ชันเก่าดู PR ที่เกี่ยวข้อง
postinstallอยู่pnpm บล็อก script ของ dependency แต่ยังคงรัน script ระดับโปรเจกต์อยู่
ขอบคุณที่แชร์ post-mortem อย่างโปร่งใส
กรณีแบบนี้สำคัญต่อทั้งอุตสาหกรรม
สงสัยว่าทราฟฟิกจากการโจมตีสามารถแยกออกจากทราฟฟิกการพัฒนาปกติได้หรือไม่
ฝั่งเราก็กำลังพยายามเพิ่ม egress filtering ในสภาพแวดล้อม dev เช่นกัน แต่ติดตรงที่
npm installพังบ่อยกำลังกังวลเรื่องความปลอดภัยของ git บนโน้ตบุ๊กส่วนตัว
ตอนนี้เก็บ SSH key ไว้ในเครื่องแล้วใช้ push
อีกทั้งยังมีสิทธิ์ผู้ดูแลระบบด้วย เลยเสี่ยงอยู่ อยากรู้ว่าทำอย่างไรให้ปลอดภัยขึ้นได้
แบบนี้จะ แยก signing key กับ access key ออกจากกัน ได้ และบัญชีผู้ดูแลก็จัดการแยกต่างหากได้
เอกสารที่เกี่ยวข้อง: 1Password SSH Agent, Git Commit Signing, GitHub OAuth, GitHub CLI Login
ข้อดีคือคีย์ไม่สามารถรั่วออกไปภายนอกได้ แต่ถ้ามัลแวร์ทำงานอยู่บนเครื่องของฉันก็ยังอันตรายอยู่ดี
ตัวอย่างบน Linux, ตัวอย่างบน macOS
อาจทำให้ยากขึ้นได้เล็กน้อยด้วย TPM หรือ Yubikey แต่ป้องกันแบบสมบูรณ์เป็นไปไม่ได้
งานที่ต้องใช้สิทธิ์ผู้ดูแลควรทำบนเครื่องเฉพาะแยกต่างหากจะปลอดภัยกว่า
gpg-agentสำหรับยืนยันตัวตน SSH ได้ด้วยเวลาจะ push หรือ commit ก็ปลดล็อกด้วยการใส่ PIN
mainและ บังคับใช้ MFA ก็จะทำให้ผู้โจมตีเข้าถึง branch สำหรับ deploy ได้ยากขึ้นทันทีcommit ที่ใช้ชื่อ Torvalds เป็น สัญลักษณ์ (signature) ที่มักพบหลังติดเชื้อ
ใน บทวิเคราะห์อย่างเป็นทางการ ของ Microsoft ก็มีการกล่าวถึง
เวิร์มตัวนี้ส่งเสียงดังมาก และผู้โจมตีบางรายก็นำ credential ที่หลุดมาไปใช้เปิดเผย repo ส่วนตัวเป็นสาธารณะหรือแก้ไข readme เพื่อใช้ประชาสัมพันธ์
ถ้าผู้โจมตีไม่ทำลายอะไร แต่ ลอบขโมยข้อมูลอย่างเงียบ ๆ แทน ก็ยังสงสัยว่าจะตรวจจับได้หรือไม่