กัน โปรเจกต์ จาก package อันตรายใน npm และ pip: ทำอะไรก่อน อะไรทีหลัง
typosquat บัญชี maintainer ถูกยึด install script dependency confusion: package อันตรายเข้ามาทางไหน และวิธีกันเรียงตามความคุ้ม สำหรับทีมเล็ก พร้อม checklist และ CI
- package อันตรายส่วนใหญ่เข้ามาทาง "เวอร์ชันใหม่ที่เพิ่งออก" ของ library ที่เราใช้อยู่แล้ว ไม่ใช่ library แปลกหน้า
- สองอย่างที่คุ้มสุดและทำได้ในวันเดียว คือ lockfile ที่ commit แล้วติดตั้งด้วย npm ci / pip --require-hashes และปิด install script เป็นค่าเริ่มต้น
- รอสามถึงเจ็ดวันก่อนรับเวอร์ชันใหม่ (cooldown) ตัดการโจมตีส่วนใหญ่ เพราะ package อันตรายมักถูกถอดภายในไม่กี่วัน
- npm audit และ pip-audit หาช่องโหว่ที่ประกาศแล้วเท่านั้น ไม่ได้บอกว่า package นั้นเป็นของปลอมหรือถูกยึด
โปรเจกต์เว็บเล็ก ๆ หนึ่งโปรเจกต์ ดึง package จาก npm มาหลายร้อยตัว โปรเจกต์ Python ก็ดึงจาก PyPI หลายสิบตัว ทุกตัวคือโค้ดของคนอื่นที่รันบนเครื่องเรา บนเซิร์ฟเวอร์ CI และบน production ปี 2025-2026 มีเหตุการณ์ใหญ่หลายครั้งที่ package ซึ่งคนใช้กันทั่วไปถูกยึดแล้วแอบใส่โค้ดขโมย token บทความนี้สรุปว่าการโจมตีแบบนี้เข้ามาทางไหน และทีมเล็กควรป้องกันอะไรก่อน เรียงตามความคุ้มต่อแรงที่ลง
คำสั่งทดสอบบน Ubuntu 24.04, Node 22 กับ npm 11, Python 3.12 กับ pip 26 ค่าตั้งและชื่อตัวเลือกเปลี่ยนได้ ตรวจเอกสารปัจจุบันก่อนใช้จริง
เข้ามาทางไหน
- typosquat ตั้งชื่อ package ให้คล้ายของจริง รอคนพิมพ์ผิดหรือ copy ชื่อผิดมาจากบทความ
- บัญชี maintainer ถูกยึด คนดูแล library ถูก phishing หรือ token หลุด ผู้โจมตีออก “เวอร์ชันใหม่” ของ library ตัวจริง ใครที่รับเวอร์ชันใหม่อัตโนมัติก็ติดทันที กรณีนี้อันตรายที่สุด เพราะชื่อถูก ที่มาถูก มีแค่เวอร์ชันล่าสุดที่เป็นพิษ
- install script npm ยอมให้ package รันคำสั่งตอนติดตั้ง (
preinstall,postinstall) และsetup.pyของ Python ก็รันโค้ดได้ แปลว่าแค่สั่งnpm installโค้ดอันตรายก็ทำงานแล้ว ไม่ต้องรอให้โปรแกรมเรา import - dependency confusion ถ้าโปรเจกต์ใช้ package ภายในชื่อ
billing-coreที่อยู่ใน registry ของบริษัท แล้วมีใครไปจดชื่อเดียวกันบน registry สาธารณะด้วยเลขเวอร์ชันสูงกว่า เครื่องมือติดตั้งที่ตั้งค่าไม่ดีอาจเลือกตัวสาธารณะ
สองเหตุการณ์ที่มีบันทึกชัด
event-stream (2018) คนดูแลเดิมส่งมอบ library ให้คนใหม่ที่อาสาเข้ามาช่วย คนใหม่เพิ่ม dependency ชื่อ flatmap-stream ซึ่งต่อมาถูกใส่โค้ดเจาะกระเป๋าเงินคริปโตเฉพาะยี่ห้อ กว่าจะพบใช้เวลาราวสองเดือน npm ถอด package ออกและเข้าดูแล event-stream เอง เมื่อ 26 พฤศจิกายน 2018 (รายละเอียดในบล็อก npm ด้านล่าง) บทเรียน คือ library ที่ “ยังใช้ได้ดี” แต่ไม่มีใครดูแลจริงเป็นจุดอ่อน
Shai-Hulud (กันยายน 2025) หนอน (worm) ที่กระจายผ่าน npm เอง เมื่อติดเครื่องนักพัฒนาคนหนึ่ง มันค้น token ของ npm, GitHub และ cloud แล้วใช้ token npm ที่ขโมยได้ไปออกเวอร์ชันใหม่ของ package ตัวอื่นที่คนนั้นดูแล กระจายต่อเป็นลูกโซ่ CISA ออกประกาศเตือน 23 กันยายน 2025 ระบุว่ากระทบมากกว่า 500 package และแนะนำให้ตรวจ lockfile หมุน credential และบังคับ MFA แบบกัน phishing หลังเหตุการณ์นี้ npm ยกเลิก token แบบเก่า (classic token) ทั้งหมดในเดือนธันวาคม 2025 และผลักให้ใช้ trusted publishing แทน
package อันตรายส่วนใหญ่ไม่ได้มาจาก library แปลกหน้า แต่มาจาก “เวอร์ชันล่าสุด” ของ library ที่เราใช้อยู่แล้ว การป้องกันจึงต้องเริ่มที่ “เราจะรับเวอร์ชันใหม่เมื่อไหร่ และยังไง”
ลำดับการป้องกัน เรียงตามความคุ้ม
1. lockfile ต้อง commit และติดตั้งจาก lockfile เท่านั้น
package-lock.json และ requirements ที่ตรึงเวอร์ชัน คือรายการบอกว่าเราตรวจและยอมรับเวอร์ชันไหนแล้ว ถ้าไม่ commit หรือติดตั้งด้วย npm install ธรรมดา เครื่อง CI อาจได้เวอร์ชันใหม่กว่าที่เราเคยเห็น
# ทีมพัฒนา (เครื่องตัวเองและ CI)
npm ci # ติดตั้งตาม package-lock.json เป๊ะ ถ้า lock ไม่ตรง package.json จะล้มเหลว ไม่แอบแก้ให้
ฝั่ง Python ใช้โหมดตรวจ hash ของ pip ทุกแพ็กเกจ รวม dependency ที่ติดมาด้วย ต้องมีทั้งเวอร์ชันและ hash ไม่ครบแม้ตัวเดียว pip จะไม่ติดตั้ง
# สร้างไฟล์ที่มี hash (ใช้ pip-tools)
pip install pip-tools
pip-compile --generate-hashes -o requirements.txt pyproject.toml
# ติดตั้งใน CI และ production
pip install --require-hashes -r requirements.txt
ตัวอย่างหนึ่งบรรทัดในไฟล์ที่ได้
requests==2.32.5 \
--hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
ได้อะไร ถ้าใครออกเวอร์ชันใหม่ของ library ตัวใดก็ตาม ระบบเราจะไม่รับเองจนกว่าจะมีคนแก้ lockfile ซึ่งเป็นการเปลี่ยนแปลงที่ review ได้ใน pull request
2. ปิด install script เป็นค่าเริ่มต้น
ตั้งที่เครื่องนักพัฒนาทุกคนและใน CI
npm config set ignore-scripts true
หลังจากนี้ npm install จะไม่รัน preinstall/postinstall ของ package ใด ส่วน npm run build หรือ npm test ของโปรเจกต์เรายังรันตามปกติ (แต่ pre/post ของสคริปต์เหล่านั้นจะไม่รัน)
ข้อแลก package ที่ต้อง compile หรือดาวน์โหลด binary ตอนติดตั้ง เช่น sharp, esbuild, @swc/core จะยังไม่พร้อมใช้ ให้ build เฉพาะตัวที่ตรวจแล้ว
npm ci
npm rebuild sharp esbuild # รายชื่อตายตัว ไม่ใช่ทุกตัว
package ดัง ๆ หลายตัวในปัจจุบันแจก binary สำเร็จรูปเป็น package แยกตามแพลตฟอร์ม ไม่ต้องรันสคริปต์ตอนติดตั้งแล้ว ให้ลองปิดสคริปต์แล้ว build ดูก่อน ตัวไหนพังค่อยเติมเข้ารายชื่อ rebuild ทีละตัว
3. รอก่อนรับเวอร์ชันใหม่ (cooldown)
package ที่ถูกยึดมักถูกรายงานและถอดภายในชั่วโมงถึงไม่กี่วัน ถ้าเราไม่ใช่คนแรกที่รับเวอร์ชันใหม่ ความเสี่ยงลดลงมาก
Dependabot มีตัวเลือก cooldown ใน .github/dependabot.yml ตามเอกสาร GitHub ปัจจุบัน ค่าเริ่มต้นสำหรับ version update คือ 3 วันแม้ไม่ได้ตั้ง และ cooldown ไม่กระทบ security update (เวอร์ชันที่ออกมาปิดช่องโหว่จะยังมาทันที)
version: 2
updates:
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
semver-major-days: 14
- package-ecosystem: pip
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
Renovate ใช้ minimumReleaseAge (ชื่อเดิม stabilityDays) เช่น "minimumReleaseAge": "7 days" และ preset config:best-practices ตั้ง 3 วันให้ npm อยู่แล้ว
ข้อควรระวัง cooldown ช่วยกับ “เวอร์ชันใหม่ที่เป็นพิษ” ไม่ช่วยกับ typosquat ที่เราพิมพ์ชื่อผิดเอง และต้องมีคนอ่าน PR ที่ bot เปิดจริง ๆ ไม่ใช่กด merge ทุกอัน
4. ตรวจที่มา (provenance) และลายเซ็น
npm package ที่ publish ผ่าน CI ด้วย trusted publishing จะมี provenance attestation บอกว่า build มาจาก repo ไหน workflow ไหน ตรวจทุก package ที่ติดตั้งอยู่ด้วย
npm audit signatures
คำสั่งนี้ตรวจลายเซ็นของ registry และ provenance ของ package ที่ดาวน์โหลดมา ถ้าลายเซ็นหายหรือผิดจะ error แต่ package ที่ยังไม่มี provenance เลยจะไม่ถูกนับว่าผิด (จึงยังไม่ใช่ตัวกันครบ) ต้องใช้ npm รุ่นใหม่ ถ้าติด Node เวอร์ชัน LTS รุ่นเก่า อาจต้องอัปเดต npm แยก
PyPI รองรับ attestation ตาม PEP 740 แล้ว โปรเจกต์ที่ publish ด้วย Trusted Publishing ผ่าน GitHub Actions ตัวมาตรฐาน (pypa/gh-action-pypi-publish) จะได้ attestation โดยอัตโนมัติ ฝั่งผู้ใช้ pip ยังไม่ตรวจ attestation ให้เองตอนติดตั้ง มีเครื่องมือ pypi-attestations และ Integrity API ให้ตรวจเอง ตรวจเอกสารปัจจุบัน
ถ้าทีมเรา publish package เองด้วย ให้ย้ายไป trusted publishing ทั้ง npm และ PyPI ไม่ต้องเก็บ token ยาว ๆ ไว้ใน secret ของ CI อีก
5. กัน dependency confusion ด้วย scoped registry
package ภายในต้องมี scope (npm) และ registry ที่ชี้ชัด
# .npmrc ในโปรเจกต์
@mycorp:registry=https://npm.internal.example/
ถ้าใช้ชื่อแบบมี scope @mycorp/billing-core และ scope นั้นชี้ไป registry ภายในเท่านั้น package สาธารณะชื่อซ้ำจะไม่ถูกดึงมาแทน ฝั่ง pip หลีกเลี่ยง --extra-index-url ที่ผสม index สาธารณะกับภายใน ใช้ index ภายในที่ทำหน้าที่ proxy ไป PyPI แทน แล้วจอง (reserve) ชื่อ package ภายในบน registry สาธารณะไว้ด้วยถ้าทำได้
6. CI ที่มีสิทธิ์น้อย และ Actions ที่ตรึง SHA
เหตุการณ์ปี 2025-2026 หลายครั้งเริ่มจาก token ใน CI ถูกขโมย ทำสองอย่าง
- ตั้ง
permissions: contents: readเป็นค่าเริ่มต้นของ workflow แล้วค่อยเพิ่มสิทธิ์เฉพาะ job ที่ต้องการ - อ้าง action ของคนอื่นด้วย commit SHA เต็ม ไม่ใช่ tag เพราะ tag ย้ายไปชี้ commit ใหม่ได้ GitHub แนะนำแบบนี้และให้ Dependabot เป็นคนอัปเดต SHA
ตัวอย่าง workflow ที่รวมข้อ 1, 2, 6 และ audit
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
permissions:
contents: read
jobs:
node:
runs-on: ubuntu-24.04
steps:
# แทน <sha> ด้วย commit SHA เต็ม 40 ตัวจาก repo ของ action นั้น และคอมเมนต์เวอร์ชันไว้
- uses: actions/checkout@<sha> # v5
- uses: actions/setup-node@<sha> # v5
with:
node-version: 22
cache: npm
- run: npm ci --ignore-scripts
- run: npm rebuild sharp # เฉพาะที่ตรวจแล้ว
- run: npm audit signatures
- run: npm audit --audit-level=high
- run: npm run build
python:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@<sha> # v5
- uses: actions/setup-python@<sha> # v6
with:
python-version: "3.12"
- run: pip install --require-hashes -r requirements.txt
- run: pip install pip-audit && pip-audit -r requirements.txt
- run: pytest
7. npm audit / pip-audit ทำอะไร และไม่ทำอะไร
ทั้งสองคำสั่งเทียบรายการ dependency กับฐานข้อมูลช่องโหว่ที่ ประกาศแล้ว (npm advisory, Python Packaging Advisory Database/OSV) มีประโยชน์กับบั๊กความปลอดภัยใน library ปกติ
สิ่งที่ไม่ทำ ไม่บอกว่า package เป็น typosquat ไม่บอกว่าเวอร์ชันล่าสุดถูกยึดจนกว่าจะมีคนรายงานและขึ้นฐานข้อมูล ซึ่งอาจเป็นหลายวันหลังเหตุ เอกสารของ pip-audit เขียนตรง ๆ ว่า ถ้าไม่กล้า pip install ก็อย่าหวังให้ pip audit ช่วย ดังนั้น audit เป็นชั้นเสริม ไม่ใช่ชั้นแรก
8. เครื่องนักพัฒนาสะอาด
เพราะหนอนอย่าง Shai-Hulud กระจายผ่านเครื่องนักพัฒนา ไม่ใช่เซิร์ฟเวอร์
- ไม่
npm install -gหรือpip installเครื่องมือแปลก ๆ ลง global ใช้npx/pipxหรือลองใน container - credential แยกตามงาน token npm/PyPI/cloud ที่ใช้ publish ไม่อยู่บนเครื่องที่ใช้ลองของ ถ้าเลี่ยงไม่ได้ ใช้ token อายุสั้น และเปิด 2FA ทุกบัญชี (npm บังคับ 2FA ตอน publish จากเครื่องแล้ว)
- ก่อนเพิ่ม library ใหม่ ดูสามอย่าง ชื่อตรงกับ repo ต้นทางไหม มียอดดาวน์โหลดและประวัติเวอร์ชันสมเหตุสมผลไหม เวอร์ชันล่าสุดออกเมื่อไหร่ ถ้าออกวันนี้ รอ
checklist
| ข้อ | ทำอะไร | แรงที่ลง | กันอะไร |
|---|---|---|---|
| 1 | commit lockfile, ติดตั้งด้วย npm ci / pip install --require-hashes | ต่ำ | เวอร์ชันใหม่ไหลเข้ามาเอง, ไฟล์ถูกแก้ระหว่างทาง |
| 2 | npm config set ignore-scripts true + rebuild รายชื่อตายตัว | ต่ำ | โค้ดรันตอนติดตั้ง |
| 3 | cooldown 3-7 วัน (Dependabot cooldown / Renovate minimumReleaseAge) | ต่ำ | เวอร์ชันพิษที่ออกใหม่ |
| 4 | npm audit signatures, trusted publishing + attestation | กลาง | package ที่ publish นอกช่องทางปกติ |
| 5 | scope + scoped registry สำหรับ package ภายใน | กลาง | dependency confusion |
| 6 | permissions: contents: read, pin Actions ด้วย SHA | กลาง | token CI หลุด, action ถูกยึด |
| 7 | npm audit / pip-audit ใน CI | ต่ำ | ช่องโหว่ที่ประกาศแล้ว (เท่านั้น) |
| 8 | ไม่ลง global มั่ว, credential แยก, 2FA | ต่ำ | หนอนที่กระจายผ่านเครื่องนักพัฒนา |
ข้อผิดพลาดที่เจอบ่อย
- มี lockfile แต่ CI ใช้
npm installทำให้ lock ถูกเขียนใหม่เงียบ ๆ - ตั้ง
ignore-scriptsแค่บนเครื่องตัวเอง ลืม CI และ Dockerfile - เปิด Dependabot แล้วตั้ง auto-merge ทุก PR เท่ากับยกเลิกประโยชน์ของ cooldown
- เชื่อว่า
npm auditสะอาด = ปลอดภัย
แหล่งอ้างอิง
- CISA: Widespread Supply Chain Compromise Impacting npm Ecosystem (23 Sep 2025)
- npm blog: Details about the event-stream incident (2018)
- npm docs: npm audit และ npm audit signatures
- pip docs: Secure installs (hash-checking mode)
- GitHub docs: Dependabot options reference (cooldown)
- PyPI docs: Attestations (PEP 740)
คำถามที่พบบ่อย
ปิด install script แล้ว package ที่ต้อง build native เช่น sharp หรือ esbuild จะพังไหม
บางตัวพัง ให้เปิดเฉพาะตัวที่จำเป็นด้วย npm rebuild ชื่อ-package หลังติดตั้ง หรือรันติดตั้งใน CI ด้วยธง --ignore-scripts แล้ว rebuild เป็นรายชื่อที่ตรวจแล้ว ปลอดภัยกว่าปล่อยให้ทุก package รันโค้ดตอนติดตั้ง
ทีมสองคน ไม่มีคนดู security เต็มเวลา เริ่มตรงไหน
ทำสามข้อแรกในตาราง (lockfile + npm ci, ignore-scripts, cooldown) ใช้เวลาไม่ถึงครึ่งวัน แล้วค่อยเพิ่ม audit ใน CI และ pin GitHub Actions เมื่อมีเวลา
คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ