Skip to content

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)

18:25:cu-central-api/main-api/package.json
  "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 ไม่ทำงาน

1:1:cu-central-api/main-api/src/lib/httpRequest.js
import axios from 'axios'
19:19:cu-central-api/main-api/src/lib/httpRequest.js
const result = await axios(options)

code import axios แต่ axios ไม่ได้อยู่ใน dependencies (มีแค่ axois typosquat) — ถ้ามี code path ใดเรียก httpRequest.js ใน prod มันจะ crash ทันที

วิธีแกะ

diff
- "axois": "0.0.1-security",
+ "axios": "^1.7.0",

แล้ว npm install + ทดสอบว่า httpRequest.js ทำงานได้


2 · error-ex git URL dependency (High · SEC-9)

25:25:cu-central-api/main-api/package.json
    "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 หรือ transfer npm install จะดึง code ใหม่โดยไม่มี integrity check
  • error-ex official อยู่บน npm เป็น versioned package — ใช้ git URL ไม่จำเป็น

วิธีแกะ

diff
- "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)

1:1:vtrc-api/api/Dockerfile
FROM node:12-alpine
1:1:cu-central-api/main-api/Dockerfile
FROM node:12-alpine

🔴 Critical — Node 12 End-of-Life ตั้งแต่ April 2022 — 4 ปีไม่มี security patch

Root cause — esm shim

27:27:vtrc-api/api/package.json
    "esm": "^3.2.25",
40:40:vtrc-api/api/package.json
    "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
  • ลบ esm shim + เปลี่ยนเป็น native ESM ("type": "module" ใน package.json)
  • ใน Go target (Volume 15) runtime tax หายไปทั้งหมด

4 · jsonwebtoken@8 CVE (High · LEG-9)

40:40:vtrc-api/api/package.json
    "jsonwebtoken": "^8.5.1",
42:42:cu-central-api/main-api/package.json
    "jsonwebtoken": "^8.5.1",

🟡 Highjsonwebtoken@8.5.1 มี CVE-2022-23529 family (algorithm confusion, prototype pollution)

  • v9+ มี patch
  • การไม่มี algorithms: ['HS256'] allow-list ใน verify (บท 9.3) ร่วมกับ v8 = vector จริงของ alg:none confusion

วิธีแกะ

diff
- "jsonwebtoken": "^8.5.1",
+ "jsonwebtoken": "^9.0.2",

ใน Go target ใช้ golang-jwt/jwt/v5


5 · ldapjs unmaintained (High · LEG-8)

30:30:cu-central-api/main-api/package.json
    "ldapjs": "^2.2.0",

🟡 Highldapjs ไม่ได้รับการดูแล fork ldapts เป็น successor ที่ยัง active

  • CVE history ของ ldapjs มีอยู่หลายตัว
  • ใน Go target ใช้ go-ldap/ldap/v3

วิธีแกะ

diff
- "ldapjs": "^2.2.0",
+ "ldapts": "^4.2.0",

6 · bcryptjs declared แต่ใช้ที่ผิดที่ (Info)

21:21:vtrc-api/api/package.json
    "bcryptjs": "^2.4.3",

การ import bcryptjs ที่เดียวใน vtrc-api คือ

2:2:vtrc-api/api/src/lib/controllers/timeAttendance/timeAttendance.js
import { hash, compare } from "bcryptjs"

🟢 Infobcryptjs 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)

41:41:vtrc-api/api/package.json
    "html-pdf": "^3.0.1",
45:45:vtrc-api/api/package.json
    "node-qpdf": "^2.1.0",

🟡 High — PDF stack ทั้งคู่ EOL

  • html-pdf ใช้ PhantomJS ที่ abandon 2018 (ตัว browser engine เอง EOL)
  • node-qpdf ไม่ maintain ตั้งแต่ 2017
  • เป็น root cause ของ execSync injection (บท 9.6) เพราะ node-qpdf build command string

วิธีแกะ

  • interim — Playwright/Puppeteer (headless Chrome)
  • ใน Go target — chromedp (browser) + pdfcpu (encryption) แยกจากกัน

8 · ไม่มี rate limit (Critical · SEC-10)

text
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

EndpointAttackTime to crack
login mutationpassword brute forceไม่จำกัด — attacker ยิงได้เต็ม network bandwidth
loginBypassbypass token brute forceถ้า RSA key เล็ก — feasible
verifyEmpCodeLoginempCode enumerationไม่จำกัด (captcha gate แค่ usingCount > 2 แล้ว reset)
refreshTokenrefresh token brute forceเลขทศนิยมค่อนข้างมาก แต่ไม่มี lockout
/readfile/prs/... (PDF)hash brute force ใน 3600s windowถ้า hash algorithm อ่อน — feasible
/webdeploymentDoS ผ่านการ trigger deployไม่จำกัด

กลไกป้องกันที่มี

กลไกเดียวคือ usingCount > 2 → captcha ที่ verifyEmpCodeLogin (บท 9.2) — แต่

  • captcha ไม่ใช่ lockout
  • apply แค่ที่ verifyEmpCodeLogin ไม่ใช่ login
  • ถ้า solve captcha ได้ (ด้วย service) gate หายไป

วิธีแกะ

javascript
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)

68:69:vtrc-api/api/src/index.js
            playground: true,
            introspection: true,

🔴 Criticalplayground: true + introspection: true โดยไม่ gate กับ IS_PRODUCTION

ผลกระทบ

  • Introspection — schema ทั้งหมด public → attacker เห็นทุก type, field, mutation, argument
  • Playground — interactive query UI ที่ prod endpoint → attacker ทดลอง query โดยไม่ต้องติดตั้งเครื่องมือ

วิธีแกะ

javascript
playground: !IS_PRODUCTION,
introspection: !IS_PRODUCTION,

10 · ไม่มี engines field (Medium · LEG-18)

text
rg '"engines"'  →  0 matches across all package.json

🟢 Medium — ไม่มี engines.node constraint ใน package.json ใด ๆ — Node version ใดก็ติดตั้ง/run ได้

วิธีแกะ

json
{
  "engines": {
    "node": ">=20.0.0"
  }
}
  • enforce ใน CI (npm ci fail ถ้า Node version ไม่ match)

11 · xlsx (SheetJS) prototype pollution (High · LEG-27)

45:45:cu-central-api/main-api/package.json
    "xlsx": "^0.16.9",

🟡 Highxlsx (SheetJS community) มี prototype pollution CVE history

วิธีแกะ

  • ใช้ SheetJS CDN build (เวอร์ชันใหม่กว่า)
  • หรือเปลี่ยนไป exceljs
  • ใน Go target — xuri/excelize

ตารารางสรุปช่องโหว่ในบทนี้

#ปัญหาSeverityFile:line
1axois typosquat🟡 Highcu-central-api/.../package.json:20
2error-ex git URL🟡 Highcu-central-api/.../package.json:25
3Node 12 EOL🔴 CriticalDockerfiles
4esm shim lock-in🔴 Criticalpackage.json:27
5jsonwebtoken@8 CVE🟡 Highpackage.json:40
6ldapjs unmaintained🟡 Highpackage.json:30
7bcryptjs ใช้ผิดที่🟢 Infopackage.json:21
8html-pdf + node-qpdf EOL🟡 Highpackage.json:41,45
9ไม่มี rate limit🔴 Critical(absent)
10GraphQL introspection + playground prod🔴 Criticalindex.js:68-69
11ไม่มี engines field🟢 Medium(absent)
12xlsx prototype pollution🟡 Highcu-central-api/.../package.json:45

อ่านต่อ → 9.11 Scorecard + remediation