- Seattle Times รอดจากการโจมตี Shai-Hulud 2.0 มาได้โดยบังเอิญ แต่ยึดหลักว่าความโชคดีไม่อาจเป็นกลยุทธ์ด้านความปลอดภัยได้ จึงนำการป้องกันฝั่งไคลเอนต์มาใช้
- การปรับปรุงของ npm อย่าง Trusted publishing / provenance / granular tokens ช่วยเสริมความแข็งแกร่งในฝั่ง “การเผยแพร่” แต่ยังคงมีช่องว่างที่ไม่สามารถหยุดการรันโค้ดอันตรายในจังหวะ “ติดตั้ง·อัปเดต” ได้
- pnpm ใช้ npm registry เดิมเหมือนเดิม แต่เพิ่มการควบคุมที่ทำให้การรันแพ็กเกจอันตรายในขั้น การใช้งาน (install/update) ทำได้ยากขึ้น
- ในโครงการนำร่อง มีการใช้การควบคุม 3 แบบของ pnpm เพื่อปิดกั้นเวกเตอร์อย่าง การรัน lifecycle scripts, การติดตั้งรีลีสล่าสุดทันที, การลดระดับความน่าเชื่อถือ ตามลำดับ
- มองข้อยกเว้นว่าไม่ใช่ความล้มเหลว แต่เป็นส่วนหนึ่งของการออกแบบ และตั้งเป้าดำเนินงานแบบ defense-in-depth ที่แม้จะมีการบันทึกข้อยกเว้นไว้ แต่เลเยอร์ที่เหลือยังคงปกป้องต่อไป
ภูมิหลังของเหตุการณ์และสมมติฐาน
- ในเดือนพฤศจิกายน 2025 เกิดกรณี npm worm แบบจำลองตัวเอง แพร่เชื้อไปยัง 796 แพ็กเกจ และกระจายผ่านปริมาณดาวน์โหลดระดับ 132 ล้านครั้งต่อเดือน
- การโจมตีใช้ สคริปต์ preinstall เพื่อขโมยข้อมูลรับรอง ติดตั้งแบ็กดอร์เพื่อคงอยู่ และในบางสภาพแวดล้อมถึงขั้นลบสภาพแวดล้อมการพัฒนา
- เหตุผลที่องค์กรไม่ได้รับผลกระทบ ไม่ใช่เพราะมีการป้องกันที่แข็งแกร่ง แต่เป็นเพราะ ความบังเอิญ ที่ไม่ได้รัน
npm install/npm update ในช่วงเวลาการโจมตี
- สำหรับองค์กรข่าว ความน่าเชื่อถือคือหัวใจหลัก และการถูกเจาะซัพพลายเชนอาจทำให้ข้อมูลลูกค้า ข้อมูลรับรองของพนักงาน อินฟราสตรักเจอร์โปรดักชัน และซอร์สโค้ดรั่วไหล อีกทั้งยังมีค่าใช้จ่ายในการกู้คืนและการแจ้งเตือนสูง
ทีมและบริบทของการนำมาใช้
- Seattle Times ใช้ npm เป็น package manager หลักมาอย่างยาวนาน และแม้เคยทดลองใช้ Yarn แต่ก็ไม่สามารถทำให้ใช้งานได้อย่างถาวร
- เหตุผลที่นำ pnpm มาใช้ คือ การควบคุมความปลอดภัยฝั่งไคลเอนต์ที่ช่วยเสริมการปรับปรุงในระดับ registry
- pnpm ถูกมองว่ามีโอกาสเปลี่ยนผ่านได้สูง เพราะเป็น drop-in replacement ที่ใช้ registry เดิม คำสั่งเดิม และ workflow เดิม
- นี่ไม่ใช่กรณีศึกษาแบบสมบูรณ์ แต่เป็นการแชร์ปัญหาและกระบวนการคิดที่ทีมจริงพบเจอระหว่างเพิ่งเริ่มต้นจัดการความปลอดภัยซัพพลายเชน
เหตุใดจึงต้องมีการควบคุมฝั่งไคลเอนต์
- การปรับปรุงด้านความปลอดภัยของ npm ทำให้ การเผยแพร่แพ็กเกจอันตรายหลังบัญชีถูกยึด ทำได้ยากขึ้นจริง
- การปรับปรุงเหล่านี้ปกป้องฝั่ง “การเผยแพร่ (publishing)” แต่ยังไม่สามารถหยุดการติดตั้งแพ็กเกจอันตรายในขั้น “การใช้งาน (consuming)” ได้
- ระหว่าง
npm install/npm update นั้น lifecycle scripts (preinstall/postinstall เป็นต้น) สามารถรันโค้ดใดก็ได้ด้วยสิทธิ์ของนักพัฒนา ก่อนจะมีการประเมินความปลอดภัยของแพ็กเกจ
- สคริปต์เหล่านี้เข้าถึงข้อมูลรับรองของ npm/GitHub/AWS/DB, ซอร์สโค้ด, คลาวด์อินฟราสตรักเจอร์ และทั้งไฟล์ซิสเต็มได้
- การโจมตีอย่าง Shai-Hulud ใช้ประโยชน์จากโครงสร้างนี้ และเมื่อบัญชีของผู้ดูแลแพ็กเกจถูกยึด สคริปต์อันตรายจะถูกรันทันที ในจังหวะติดตั้ง ทำให้เกิดความเสียหายก่อนที่ชุมชนจะตรวจพบ
- การปรับปรุงฝั่งเผยแพร่ของ npm + การควบคุมฝั่งใช้งานของ pnpm จึงถูกผูกเป็นการป้องกันแบบ เสริมกัน เพื่อสร้างเป็น “defense-in-depth”
3 เลเยอร์ที่นำมาใช้
- ในโครงการนำร่อง ใช้การควบคุม 3 แบบร่วมกันเพื่อรับมือกับเวกเตอร์การโจมตีที่แตกต่างกัน
- การควบคุมแต่ละแบบมีทางออกสำหรับข้อยกเว้นที่เกิดขึ้นจริง และออกแบบบนสมมติฐานว่าในสภาพแวดล้อมจริงย่อมต้องมีข้อยกเว้น
Control 1: การจัดการ Lifecycle Script
- pnpm สามารถ บล็อก lifecycle scripts ได้เป็นค่าเริ่มต้น และยังให้การติดตั้งดำเนินต่อพร้อมคำเตือนได้
- เพราะกังวลว่าคำเตือนอาจถูกมองข้าม จึงเลือก
strictDepBuilds: true เพื่อบังคับให้ถ้ามีสคริปต์ การติดตั้งจะ ล้มเหลวทันที
- ตัวอย่างการตั้งค่าใน
pnpm-workspace.yaml มีฟิลด์ดังนี้
strictDepBuilds: true
onlyBuiltDependencies: allowlist ของแพ็กเกจที่มี build script ที่จำเป็น
ignoredBuiltDependencies: รายชื่อแพ็กเกจที่มี build script ที่ไม่จำเป็นและต้องการบล็อก (หรือเพิกเฉย)
- “สคริปต์ที่จำเป็น” ถูกนิยามว่าเป็นการทำงานอย่างการคอมไพล์ native extension หรือการลิงก์ไลบรารีที่ขึ้นกับแพลตฟอร์ม
- “สคริปต์ที่ไม่จำเป็น” หมายถึงการปรับแต่งเพื่อเพิ่มประสิทธิภาพหรือการตั้งค่าเสริม ซึ่งในรูปแบบการใช้งานของทีมไม่กระทบต่อฟังก์ชัน
- การทำให้การติดตั้งล้มเหลวบังคับให้ต้องทำขั้นตอนต่อไปนี้
- pnpm ระบุได้อย่างชัดเจนว่าแพ็กเกจใดมีสคริปต์
- ตรวจสอบและทำความเข้าใจพฤติกรรมของสคริปต์
- ตัดสินใจและบันทึกอย่างมีสติด้วยการพิจารณาของมนุษย์ว่าจะอนุญาตหรือบล็อก
- ทีม pnpm กำลังพิจารณาให้
strictDepBuilds: true เป็นค่าเริ่มต้นใน v11 และกำลังทบทวนการปรับปรุงชื่อของไวยากรณ์ allow/deny ด้วย
Control 2: Release Cooldown
- เวอร์ชันที่เพิ่งถูกเผยแพร่จะถูกกันไม่ให้ติดตั้งในช่วง cooldown ที่กำหนด เพื่อเปิดเวลาให้ชุมชนตรวจจับและถอดถอนแพ็กเกจอันตรายได้
- ตัวอย่างการตั้งค่าใน
pnpm-workspace.yaml มีฟิลด์ดังนี้
minimumReleaseAge: <duration-in-minutes>
minimumReleaseAgeExclude: รายการข้อยกเว้น เช่น hotfix ด่วน
- ต้องเปลี่ยนวิธีคิดจากนิสัยที่ว่า “ใหม่ล่าสุดดีที่สุด” ไปสู่มุมมองด้านซัพพลายเชนที่ว่า เวอร์ชันที่เก่ากว่าเล็กน้อยอาจปลอดภัยกว่า
- ในการโจมตีเดือนกันยายน 2025 (16 แพ็กเกจรวมถึง debug และ chalk) ใช้เวลาถอดถอนราว 2.5 ชั่วโมง และในกรณี Shai-Hulud 2.0 เดือนพฤศจิกายน 2025 ใช้เวลาราว 12 ชั่วโมง
- ตามระดับการยอมรับความเสี่ยงของแต่ละองค์กร ช่วง cooldown อาจเป็นระดับชั่วโมง/วัน/สัปดาห์ และไม่ว่าแบบใดก็น่าจะป้องกันการโจมตีดังกล่าวได้
- แนวทางนี้สอดคล้องกับความเป็นจริงที่องค์กรเองก็ไม่ได้ใช้เวอร์ชันล่าสุดเสมออยู่แล้ว จึงไม่รบกวนงานมากนัก
- เมื่อจำเป็นจริง เช่น security patch หรือบั๊กวิกฤต ก็สามารถตรวจสอบแล้วปลดข้อยกเว้นได้
Control 3: นโยบายความน่าเชื่อถือ
- หากมีเวอร์ชันที่เผยแพร่ด้วยการยืนยันตัวตนที่อ่อนแอกว่าเวอร์ชันก่อนหน้า จะบล็อกการติดตั้ง
- อธิบายว่านี่เป็นสัญญาณของกรณีที่บัญชีผู้ดูแลถูกยึด และมีการเผยแพร่จากเครื่องของผู้โจมตีแทนที่จะเป็น CI/CD อย่างเป็นทางการ
- ตัวอย่างการตั้งค่าใน
pnpm-workspace.yaml มีฟิลด์ดังนี้
trustPolicy: no-downgrade
trustPolicyExclude: รายการข้อยกเว้น เช่น การย้ายระบบ CI/CD
- อธิบายว่า npm ติดตามระดับความน่าเชื่อถือของการเผยแพร่แพ็กเกจไว้ 3 ระดับ (สูง→ต่ำ)
- Trusted Publisher: อิงจาก GitHub Actions + OIDC + npm provenance
- Provenance: attestation ที่เซ็นชื่อจาก CI/CD
- No Trust Evidence: การเผยแพร่ด้วย username/password หรือ token
- หากเวอร์ชันใหม่มีระดับความน่าเชื่อถือต่ำกว่าเวอร์ชันก่อนหน้า การติดตั้งจะล้มเหลว
- ในการโจมตี s1ngularity เดือนสิงหาคม 2025 ซึ่งผู้โจมตีเผยแพร่เวอร์ชันอันตรายจากเครื่อง local โดยไม่มีการเข้าถึง CI/CD และจึงไม่มี provenance การควบคุมนี้ก็น่าจะบล็อกการติดตั้งได้
- ตัวอย่างกรณีที่การลดระดับอาจเกิดขึ้นอย่างถูกต้อง ได้แก่ การมีผู้ดูแลคนใหม่ การย้ายระบบ CI/CD หรือ hotfix แบบ manual ระหว่างที่ CI/CD ขัดข้อง ซึ่งหลังตรวจสอบแล้วสามารถเพิ่มลงในรายการข้อยกเว้นได้
- ฟีเจอร์นี้เป็น ฟีเจอร์ใหม่ ที่เพิ่มเข้ามาใน pnpm เมื่อเดือนพฤศจิกายน 2025 และยังอยู่ระหว่างเรียนรู้ว่าการลดระดับที่ถูกต้องตามกฎหมายเกิดขึ้นบ่อยเพียงใดในทางปฏิบัติ
ตัวอย่างการทำงานร่วมกันของเลเยอร์: การแพตช์ช่องโหว่ React
- ในกรณีที่ต้องรีบใช้แพตช์ของ ช่องโหว่วิกฤต ใน React Server Components ซึ่งเปิดเผยเมื่อเดือนธันวาคม 2025 ทันที
- โดยปกติ cooldown จะกันไม่ให้ติดตั้ง “เวอร์ชันที่เพิ่งเผยแพร่” แต่เมื่อเป็น security patch ระดับวิกฤตก็ไม่สามารถรอได้
- ในกรณีนี้ สามารถเพิ่ม React เวอร์ชันนั้นลงใน
minimumReleaseAgeExclude ได้ แต่จะต้องตรวจสอบประกาศช่องโหว่และความชอบธรรมของแพตช์ก่อนจึงค่อยใช้ข้อยกเว้น
- แม้ใช้ข้อยกเว้นแล้ว เลเยอร์อื่นก็ยังคงปกป้องต่อไป
- โดยทั่วไป React ไม่มี lifecycle scripts ดังนั้นหากเวอร์ชันแพตช์มีสคริปต์เพิ่มเข้ามา ก็จะเป็นสัญญาณน่าสงสัยทันทีและอาจถูกบล็อกได้
- หากผู้โจมตีขโมยข้อมูลรับรองแล้วเผยแพร่ “แพตช์” จากเครื่อง local ก็อาจถูกบล็อกด้วยการลดระดับความน่าเชื่อถือ
- ข้อยกเว้นจึงไม่ใช่ “ความล้มเหลวด้านความปลอดภัย” แต่เป็นการออกแบบที่แม้เลี่ยงเลเยอร์หนึ่งไปได้ ก็ยังมีเลเยอร์อื่นเหลืออยู่ ทำให้ ไม่มี single point of failure
ผลการนำร่องใช้งาน
- มีการทำ PoC โดยใช้การควบคุมทั้ง 3 แบบกับบริการแบ็กเอนด์หนึ่งตัว
- เวลาที่ใช้เตรียมทั้งหมดตั้งแต่การตรวจสอบ ทำความเข้าใจ ไปจนถึงกำหนดแนวทางการเข้าถึง อยู่ในระดับไม่กี่ชั่วโมง
- pnpm ระบุแพ็กเกจที่มี lifecycle scripts ได้ 3 ตัว
- esbuild: ช่วยเพิ่มประสิทธิภาพการเริ่มต้น CLI ในระดับมิลลิวินาที แต่ทีมใช้เฉพาะ JS API จึงมองว่าไม่จำเป็น
- @firebase/util: เป็นการตั้งค่าอัตโนมัติของ client SDK แต่ทีมใช้เฉพาะ server SDK จึงมองว่าไม่จำเป็น
- protobufjs: เป็นการตรวจสอบความเข้ากันได้ของสคีมา ใช้เพียงในฐานะ transitive dependency และไม่จำเป็นสำหรับกรณีใช้งานของทีม
- หลังจากตรวจเอกสารและวิเคราะห์สคริปต์ (รวมถึงใช้ AI ช่วยตีความสคริปต์) ก็สรุปว่าสคริปต์ทั้งสามไม่จำเป็นสำหรับกรณีใช้งานของทีม และจึงบล็อกทั้งหมด
- ไม่พบผลกระทบต่อฟังก์ชันการทำงาน
- friction เป็นฟีเจอร์ที่ตั้งใจให้มี เพื่อบังคับไม่ให้ เชื่อถือโค้ดที่รันในสภาพแวดล้อมโดยปริยาย
- หาก dependency ใหม่มีสคริปต์ คาดว่าจะใช้เวลาราว 15 นาทีในการตรวจสอบและบันทึก
สิ่งที่เรียนรู้ระหว่างการใช้งานจริง
- การจับคู่เลเยอร์ฝั่ง client-side กับการปรับปรุงฝั่ง publishing-side ของ npm ทำให้รู้สึกได้ว่า defense-in-depth ใช้งานได้จริง
- แม้มีการใช้ข้อยกเว้น ก็ยังมีเลเยอร์อื่นคงอยู่ จึงลดความกังวลต่อข้อยกเว้นลงได้
- การเปลี่ยน mental model จาก “ความสะดวกมาก่อน” ไปเป็น “ความปลอดภัยมาก่อน” ต้องใช้เวลา แต่เมื่อคุ้นเคยแล้วจะรู้สึกเป็นธรรมชาติ
- แม้องค์กรขนาดกลางที่ไม่มีทีมความปลอดภัยเฉพาะทางก็สามารถนำไปใช้ได้อย่างเป็นรูปธรรม
- trust policy เป็นฟีเจอร์ที่เพิ่งเปิดใช้ได้ไม่กี่สัปดาห์ จึงยังต้องเรียนรู้เพิ่มเติมทั้งเรื่องความถี่ของการลดระดับที่ถูกต้อง และแนวทางปฏิบัติจริงในการใช้งาน
- มีแผนจะขยายไปยังโค้ดเบสอื่นในเร็ว ๆ นี้ และจะได้ข้อมูลเพิ่มเติมจากแอปพลิเคชันที่มี dependency graph ต่างกัน
เคล็ดลับสำหรับทีมอื่น
- แนะนำให้เริ่มจากโปรเจกต์เดียวก่อน เพื่อเรียนรู้ workflow และจุดที่เกิด friction
- ทั้ง lifecycle scripts, release cooldown และ trust downgrade ต่างก็อาจต้องมีข้อยกเว้น จึงควร ออกแบบโดยสมมติว่าต้องมีข้อยกเว้นตั้งแต่แรก
- แนะนำกลยุทธ์ใช้
strictDepBuilds: true ตั้งแต่วันแรก เพื่อบังคับด้วยการทำให้ติดตั้งล้มเหลว แทนแนวทางที่อิงคำเตือน
- ควรบันทึกข้อยกเว้นทั้งหมดไว้ เพื่อสร้าง audit trail และทำให้จัดการภายหลังได้ง่าย
- จำไว้ว่าข้อยกเว้นในเลเยอร์หนึ่ง ไม่ได้ทำให้การปกป้องจากเลเยอร์อื่นหายไป
1 ความคิดเห็น
pnpm! pnpm! pnpm ! สมกับที่เชื่อใจอยู่จริงๆ