3 คะแนน โดย GN⁺ 2025-12-19 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 ความคิดเห็น

 
bichi 2025-12-19

pnpm! pnpm! pnpm ! สมกับที่เชื่อใจอยู่จริงๆ