บทความ › ความปลอดภัย

กัน โปรเจกต์ จาก package อันตรายใน npm และ pip: ทำอะไรก่อน อะไรทีหลัง

typosquat บัญชี maintainer ถูกยึด install script dependency confusion: package อันตรายเข้ามาทางไหน และวิธีกันเรียงตามความคุ้ม สำหรับทีมเล็ก พร้อม checklist และ CI

กลาง อ่าน 14 นาที 21 ตุลาคม 2569

อ่านจบแล้วจะรู้
  • 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 ที่เราใช้อยู่แล้ว การป้องกันจึงต้องเริ่มที่ “เราจะรับเวอร์ชันใหม่เมื่อไหร่ และยังไง”

lockfile + npm ci / --require-hashesได้เวอร์ชันเดิมทุกครั้งปิด install scriptsสคริปต์แปลกไม่รันตอนติดตั้งรอ 3-7 วันก่อนรับเวอร์ชันใหม่แพ็กเกจถูกยึดมักถูกถอนในไม่กี่วันตรวจ provenance + registry ส่วนตัวของจริงจากผู้ดูแลจริง ไม่สับสนชื่อ
เรียงตามผลตอบแทนต่อแรง: lockfile มาก่อน

ลำดับการป้องกัน เรียงตามความคุ้ม

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

ข้อทำอะไรแรงที่ลงกันอะไร
1commit lockfile, ติดตั้งด้วย npm ci / pip install --require-hashesต่ำเวอร์ชันใหม่ไหลเข้ามาเอง, ไฟล์ถูกแก้ระหว่างทาง
2npm config set ignore-scripts true + rebuild รายชื่อตายตัวต่ำโค้ดรันตอนติดตั้ง
3cooldown 3-7 วัน (Dependabot cooldown / Renovate minimumReleaseAge)ต่ำเวอร์ชันพิษที่ออกใหม่
4npm audit signatures, trusted publishing + attestationกลางpackage ที่ publish นอกช่องทางปกติ
5scope + scoped registry สำหรับ package ภายในกลางdependency confusion
6permissions: contents: read, pin Actions ด้วย SHAกลางtoken CI หลุด, action ถูกยึด
7npm audit / pip-audit ใน CIต่ำช่องโหว่ที่ประกาศแล้ว (เท่านั้น)
8ไม่ลง global มั่ว, credential แยก, 2FAต่ำหนอนที่กระจายผ่านเครื่องนักพัฒนา

ข้อผิดพลาดที่เจอบ่อย

  • มี lockfile แต่ CI ใช้ npm install ทำให้ lock ถูกเขียนใหม่เงียบ ๆ
  • ตั้ง ignore-scripts แค่บนเครื่องตัวเอง ลืม CI และ Dockerfile
  • เปิด Dependabot แล้วตั้ง auto-merge ทุก PR เท่ากับยกเลิกประโยชน์ของ cooldown
  • เชื่อว่า npm audit สะอาด = ปลอดภัย

แหล่งอ้างอิง

คำถามที่พบบ่อย

ปิด 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 เมื่อมีเวลา

คำสั่งและค่าตั้งในบทความทดสอบบนระบบตัวอย่าง ลองบนเครื่องทดสอบก่อนใช้กับระบบจริงเสมอ

อ่านต่อ

#Securityกลาง

กัน CSRF โดยไม่ต้องใช้ token ด้วย Sec-Fetch-Site และ Origin (Go 1.25)

เบราว์เซอร์ยุคนี้บอกเซิร์ฟเวอร์เองว่าคำขอมาจากเว็บอื่นหรือเปล่า เช็กหัว Sec-Fetch-Site กับ Origin แทน CSRF token ได้ วิธีทำใน Go 1.25 และข้อแม้ที่ต้องรู้

อ่าน 10 นาที
#Securityกลาง

Post-quantum บน SSH และ HTTPS: ML-KEM คืออะไร และเช็กเซิร์ฟเวอร์ของเรายังไง

OpenSSH 10 ใช้ mlkem768x25519 เป็นค่าเริ่มต้นแล้ว 10.1 เตือนเมื่อไม่ได้ใช้ เบราว์เซอร์ก็ใช้ X25519MLKEM768 วิธีเช็กว่าเซิร์ฟเวอร์ตกลงใช้อะไรจริง และต้องอัปเกรดอะไร

อ่าน 8 นาที
#Securityเริ่มต้น

ใบรับรอง SSL/TLS อายุสั้นลงเหลือ 47 วัน: ไทม์ไลน์ 2026-2029 และต้องทำอะไร

CA/B Forum ลดอายุใบรับรอง TLS จาก 398 วันเหลือ 47 วันภายในปี 2029 ส่วน Let's Encrypt มีใบ 6 วันและใบสำหรับ IP แล้ว สรุปไทม์ไลน์ และวิธีตั้งต่ออายุอัตโนมัติ

อ่าน 9 นาที