10.10 · Supply chain + runtime
บทนี้ catalog ความเสี่ยงที่มาจาก dependency และ runtime environment — typosquat package, git-URL dependency, Node.js 12 ที่ EOL มา 4 ปี, jsonwebtoken@8 ที่มี CVE, และการไม่มี rate limit ทุก endpoint
1 · axois typosquat (High · SEC-7)
"dependencies": {
"apollo-server": "^2.16.1",
"axois": "0.0.1-security",
"body-parser": "^1.19.0",
"date-diff": "^0.2.2",
"dotenv": "^8.2.0",
"email-templates": "^7.1.1",
"error-ex": "git+https://github.com/Qix-/node-error-ex.git",🟡 High — "axois": "0.0.1-security" เป็น typosquat ของ axios
ทำไมรุนแรง
- เวอร์ชัน
0.0.1-securityเป็น npm-reserved "security placeholder" ที่ npm ตีพิมพ์เมื่อชื่อ package ถูก hijack หรือ typosquat — npm security ได้ flag ไว้แล้ว - แม้ codebase จะไม่ได้
import 'axois'จริง (grep confirms 0 imports) แต่npm installยัง pull package เข้าnode_modulesและ install graph - ถ้า typosquat package มี malicious postinstall script มัน execute ตอน
npm install
ปัญหาเพิ่มเติม — httpRequest.js ไม่ทำงาน
import axios from 'axios'const result = await axios(options)code import axios แต่ axios ไม่ได้อยู่ใน dependencies (มีแค่ axois typosquat) — ถ้ามี code path ใดเรียก httpRequest.js ใน prod มันจะ crash ทันที
วิธีแกะ
- "axois": "0.0.1-security",
+ "axios": "^1.7.0",แล้ว npm install + ทดสอบว่า httpRequest.js ทำงานได้
2 · error-ex git URL dependency (High · SEC-9)
"error-ex": "git+https://github.com/Qix-/node-error-ex.git",🟡 High — pin dependency ไปที่ git URL แทน npm version
ทำไมรุนแรง
git+https://ดึง HEAD ของ default branch — mutable ref ไม่มี hash protection- ถ้า upstream repo (
Qix-/node-error-ex) ถูก compromise หรือ transfernpm installจะดึง code ใหม่โดยไม่มี integrity check error-exofficial อยู่บน npm เป็น versioned package — ใช้ git URL ไม่จำเป็น
วิธีแกะ
- "error-ex": "git+https://github.com/Qix-/node-error-ex.git",
+ "error-ex": "^1.3.2",3 · Node.js 12 EOL (Critical · LEG-1, LEG-2)
FROM node:12-alpineFROM node:12-alpine🔴 Critical — Node 12 End-of-Life ตั้งแต่ April 2022 — 4 ปีไม่มี security patch
Root cause — esm shim
"esm": "^3.2.25", "start": "node -r esm ."esm shim (ตัวช่วยให้ Node 12 รัน ESM syntax) ไม่ได้รับการดูแลตั้งแต่ 2019 — ทำให้ codebase ติดกับดัก Node 12
ผลกระทบ
- ทุก CVE ของ Node 12 ตั้งแต่เมษายน 2022 ยังไม่มี patch (มีหลายสิบ)
npm installของ package ใหม่ที่ต้องการ Node 14+ จะ fail- การใช้ native addon ที่ compile บน Node 12 ไม่สามารถ migrate ได้ง่าย
วิธีแกะ
- Bump เป็น Node 20 LTS
- ลบ
esmshim + เปลี่ยนเป็น native ESM ("type": "module"ใน package.json) - ใน Go target (Volume 15) runtime tax หายไปทั้งหมด
4 · jsonwebtoken@8 CVE (High · LEG-9)
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",🟡 High — jsonwebtoken@8.5.1 มี CVE-2022-23529 family (algorithm confusion, prototype pollution)
- v9+ มี patch
- การไม่มี
algorithms: ['HS256']allow-list ใน verify (บท 9.3) ร่วมกับ v8 = vector จริงของalg:noneconfusion
วิธีแกะ
- "jsonwebtoken": "^8.5.1",
+ "jsonwebtoken": "^9.0.2",ใน Go target ใช้ golang-jwt/jwt/v5
5 · ldapjs unmaintained (High · LEG-8)
"ldapjs": "^2.2.0",🟡 High — ldapjs ไม่ได้รับการดูแล fork ldapts เป็น successor ที่ยัง active
- CVE history ของ
ldapjsมีอยู่หลายตัว - ใน Go target ใช้
go-ldap/ldap/v3
วิธีแกะ
- "ldapjs": "^2.2.0",
+ "ldapts": "^4.2.0",6 · bcryptjs declared แต่ใช้ที่ผิดที่ (Info)
"bcryptjs": "^2.4.3",การ import bcryptjs ที่เดียวใน vtrc-api คือ
import { hash, compare } from "bcryptjs"🟢 Info — bcryptjs declare แต่ใช้แค่ใน time-attendance subsystem ไม่ใช่ primary auth (บท 9.2 อธิบายว่า primary auth forward plaintext password ไป cu-central-api เพื่อ LDAP/AD bind — ไม่ใช่ NestJS centralize-api)
Primary login ไม่พึ่ง bcrypt ใน vtrc-api — credential check อยู่ที่ LDAP/AD ของ cu-central-api
7 · html-pdf + node-qpdf unmaintained (High · LEG-10)
"html-pdf": "^3.0.1", "node-qpdf": "^2.1.0",🟡 High — PDF stack ทั้งคู่ EOL
html-pdfใช้ PhantomJS ที่ abandon 2018 (ตัว browser engine เอง EOL)node-qpdfไม่ maintain ตั้งแต่ 2017- เป็น root cause ของ
execSyncinjection (บท 9.6) เพราะnode-qpdfbuild command string
วิธีแกะ
- interim — Playwright/Puppeteer (headless Chrome)
- ใน Go target —
chromedp(browser) +pdfcpu(encryption) แยกจากกัน
8 · ไม่มี rate limit (Critical · SEC-10)
grep -r 'express-rate-limit' . → 0 hits
grep -r 'rate-limiter' . → 0 hits🔴 Critical — ไม่มี express-rate-limit, rate-limiter-flexible, express-slow-down, หรือ limit_req ใน nginx config (audit ที่ vtrc-api/nginx/default.conf มีแค่ TLS cipher config)
Endpoint ที่ brute-forceable
| Endpoint | Attack | Time to crack |
|---|---|---|
login mutation | password brute force | ไม่จำกัด — attacker ยิงได้เต็ม network bandwidth |
loginBypass | bypass token brute force | ถ้า RSA key เล็ก — feasible |
verifyEmpCodeLogin | empCode enumeration | ไม่จำกัด (captcha gate แค่ usingCount > 2 แล้ว reset) |
refreshToken | refresh token brute force | เลขทศนิยมค่อนข้างมาก แต่ไม่มี lockout |
/readfile/prs/... (PDF) | hash brute force ใน 3600s window | ถ้า hash algorithm อ่อน — feasible |
/webdeployment | DoS ผ่านการ trigger deploy | ไม่จำกัด |
กลไกป้องกันที่มี
กลไกเดียวคือ usingCount > 2 → captcha ที่ verifyEmpCodeLogin (บท 9.2) — แต่
- captcha ไม่ใช่ lockout
- apply แค่ที่
verifyEmpCodeLoginไม่ใช่login - ถ้า solve captcha ได้ (ด้วย service) gate หายไป
วิธีแกะ
import rateLimit from 'express-rate-limit'
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 min
max: 10, // 10 attempts per IP
standardHeaders: true,
legacyHeaders: false
})
app.use('/api/graphql', loginLimiter)ใน Go target — ulule/limiter หรือ Redis-backed token bucket
9 · GraphQL introspection + playground ใน prod (Critical · SEC-2)
playground: true,
introspection: true,🔴 Critical — playground: true + introspection: true โดยไม่ gate กับ IS_PRODUCTION
ผลกระทบ
- Introspection — schema ทั้งหมด public → attacker เห็นทุก type, field, mutation, argument
- Playground — interactive query UI ที่ prod endpoint → attacker ทดลอง query โดยไม่ต้องติดตั้งเครื่องมือ
วิธีแกะ
playground: !IS_PRODUCTION,
introspection: !IS_PRODUCTION,10 · ไม่มี engines field (Medium · LEG-18)
rg '"engines"' → 0 matches across all package.json🟢 Medium — ไม่มี engines.node constraint ใน package.json ใด ๆ — Node version ใดก็ติดตั้ง/run ได้
วิธีแกะ
{
"engines": {
"node": ">=20.0.0"
}
}- enforce ใน CI (
npm cifail ถ้า Node version ไม่ match)
11 · xlsx (SheetJS) prototype pollution (High · LEG-27)
"xlsx": "^0.16.9",🟡 High — xlsx (SheetJS community) มี prototype pollution CVE history
วิธีแกะ
- ใช้ SheetJS CDN build (เวอร์ชันใหม่กว่า)
- หรือเปลี่ยนไป
exceljs - ใน Go target —
xuri/excelize
ตารารางสรุปช่องโหว่ในบทนี้
| # | ปัญหา | Severity | File:line |
|---|---|---|---|
| 1 | axois typosquat | 🟡 High | cu-central-api/.../package.json:20 |
| 2 | error-ex git URL | 🟡 High | cu-central-api/.../package.json:25 |
| 3 | Node 12 EOL | 🔴 Critical | Dockerfiles |
| 4 | esm shim lock-in | 🔴 Critical | package.json:27 |
| 5 | jsonwebtoken@8 CVE | 🟡 High | package.json:40 |
| 6 | ldapjs unmaintained | 🟡 High | package.json:30 |
| 7 | bcryptjs ใช้ผิดที่ | 🟢 Info | package.json:21 |
| 8 | html-pdf + node-qpdf EOL | 🟡 High | package.json:41,45 |
| 9 | ไม่มี rate limit | 🔴 Critical | (absent) |
| 10 | GraphQL introspection + playground prod | 🔴 Critical | index.js:68-69 |
| 11 | ไม่มี engines field | 🟢 Medium | (absent) |
| 12 | xlsx prototype pollution | 🟡 High | cu-central-api/.../package.json:45 |
อ่านต่อ → 9.11 Scorecard + remediation