3.2.8 · Security + debt
บทนี้ catalog ความเสี่ยงด้านความปลอดภัยและ technical debt ของ cu-central-api โดยเน้นสิ่งที่ต่างจาก vtrc-api (ส่วนที่เหมือนกัน cover ใน Volume 12 Security แล้ว)
Severity summary
| Severity | Count |
|---|---|
| 🔴 Critical | 6 |
| 🟡 High | 7 |
| 🟢 Medium | 5 |
🔴 Critical
SEC-CU-01 · JWT_SECRET default empty string
JWT_ISSUER = '',
JWT_SECRET = '',const getPrivayeKey = () => {
return JWT_SECRET
}🔴 default เป็น empty string — และ jsonwebtoken@8.5.1 ยอม sign ด้วย empty string (ไม่ throw)
ถ้า env unset ใน deployment ใด — service จะ boot และ sign token จริงด้วย secret "" ทำให้ทุกคนสามารถ forge JWT ได้ (เพราะเข้ารหัสด้วย secret ที่เป็น public knowledge)
ไม่มี guard แบบ if (IS_PRODUCTION && !JWT_SECRET) throw ที่จะ hard-fail ที่ boot
typo getPrivayeKey (ควรเป็น getPrivateKey) — ไม่มี impact แต่บ่งบอกว่าไม่มี code review ที่จริงจัง
Fix: Rotate JWT_SECRET เป็น random string ที่ strong และเพิ่ม guard
if (IS_PRODUCTION && (!JWT_SECRET || JWT_SECRET.length < 32)) {
throw new Error('JWT_SECRET must be set to a strong value in production')
}SEC-CU-02 · apiKeys hardcoded ใน code
const allowedApiKeys = [
{
apiKey: 'OGJlYTEzOTBjMzk3NjlmYWQ1ZTcxZDZhN2NhZWMyYTk3YTQ2NGIzMDhiMGM4NmRjM2NlNjZkM2I2ZDIyOGQyMQ',
appId: 'acwfa3t-wafbAS-fawkgt3948yf-egrhe3fwe',
appName: 'VTRC_APP',
level: 1
},
{
apiKey: '562B132B1E87695A052717C20336CDA57FBC4C5B19EF31356CC07C5A43656DE47DEFE452D241C3F443DE54',
appId: '7d962c5b-68d1-4a8f-849d-4d617711d94b',
appName: 'VTRC_ADMIN_APP',
level: 4
}
](apiKeys ที่ commit — detail ใน 3.2.4)
Fix: ย้ายไป env หรือ DB พร้อม rotation
SEC-CU-03 · .env.develop-test committed
ไฟล์ .env.develop-test ที่มี UAT secrets ทั้งหมด commit อยู่ใน repo — cover ใน Volume 12 Security (cu-central-api inventory)
ตัวอย่าง secrets ที่รั่ว
AES_SECRET_KEY="EoABvcD7RsFBeKRZgSOHS4LLD08To6Pc"
AES_INIT_VECTOR="AaNZRKzytx9yCAiA"Fix: Purge จาก git history + rotate secrets ทั้งหมด
SEC-CU-04 · /auth/signinbypass — passwordless signin
app.post(`${API_PREFIX_PATH}/auth/signinbypass`, urlencodedParser, async (req, res) => {
const params = req.body
const signInResult = await Auth.signInByPass(params.username, params.org, params.apiKey)
res.send(JSON.stringify(signInResult)).end()
})endpoint ที่ mint token เต็มโดยไม่ตรวจ credential — live ใน production image (detail ใน 3.2.3)
Fix: gate ด้วย IS_PRODUCTION หรือลบออกจาก routes
SEC-CU-05 · ignoreAuth bypass
if (ignoreAuth) {
return resolve.apply(this, args)
} ): SearchUserResponse! @auth(accessLevel: 4, ignoreAuth: true)apiKey อย่างเดียวพอ สำหรับ field ที่ใช้ ignoreAuth: true — detail ใน 3.2.4
Fix: ลบ ignoreAuth arg ออกจาก directive + ใช้ service-to-service auth ที่มี session แทน
SEC-CU-06 · rejectUnauthorized: false ใน LDAP
const tlsOptions = {
rejectUnauthorized: false,
// cert: cer,
// key
}🔴 ปิด TLS cert verification บน LDAP connection — MITM สามารถ intercept password ของ user ระหว่าง bind
สังเกตว่ามี comment // cert: cer และ // key ที่ import fs.readFileSync แต่ comment ไว้ — แสดงว่าทีมเคย plan จะใช้ cert จริง แต่ยังไม่ implement
Fix: ใส่ cert path จริง + เปลี่ยนเป็น rejectUnauthorized: true
LEG-CU-01 · axois typosquat
"axois": "0.0.1-security",🔴 typosquat ของ axios — npm reserve 0.0.1-security สำหรับ typosquatted packages
- codebase ไม่ได้ import
axoisจริงใน business logic (ยกเว้นlib/httpRequest.js:1ที่เขียนimport axois from 'axios'— module resolve หาaxiospackage ที่ไม่มี) - แต่
npm installยัง pull package เข้าnode_modules - ถ้า typosquat มี postinstall script → execute ตอน install
CORR-CU-01 · httpRequest.js ใช้ axios ที่ไม่ได้ declare
import axois from 'axios' const sent = await axois.request(requestObj)- import
axios(จากชื่อไฟล์เขียนaxoisแต่ from'axios') แต่ dependencies มีแค่axois(typo) - ถ้ามี code path ใดถึง
httpRequest.js— crash ทันทีเพราะaxiosmodule ไม่ถูกประกาศ
httpRequest.js ถูก import โดย lib/sms.js:3 (import { httpPost } from './httpRequest') — ถ้า SMS code path ทำงาน (ส่ง OTP หรือ notification) จะ crash
ส่วน lib/his/healCardDiscount/healCardDiscount.repo.js:1 import axios ตรง ๆ — ก็ crash เช่นกันเพราะไม่มี dep
🟡 High
LEG-CU-02 · error-ex git URL dependency
"error-ex": "git+https://github.com/Qix-/node-error-ex.git",🟡 pin ไปที่ git URL แทน npm version — mutable ref ไม่มี hash protection ถ้า upstream repo compromised
LEG-CU-03 · ldapjs unmaintained
"ldapjs": "^2.2.0",🟡 project archived ตั้งแต่ 2023 — fork ldapts เป็น successor ที่ active
LEG-CU-04 · jsonwebtoken@8 CVE
"jsonwebtoken": "^8.5.1",🟡 8.x EOL — CVE-2022-23529 family มี patch ใน v9+
LEG-CU-05 · Node 12 + esm shim
FROM node:12.18.3 "esm": "^3.2.25",🟡 Node 12 EOL April 2022 — 4+ ปีไม่มี security patch
esm shim (last release 2019) เป็น root cause ที่ lock codebase ไว้ที่ Node 12
LEG-CU-06 · xlsx CVE history
"xlsx": "^0.16.9",🟡 SheetJS community มี prototype pollution CVE history — เวอร์ชันปัจจุบันอยู่บน SheetJS CDN (ไม่ใช่ npm)
CORR-CU-02 · OTP inconsistency
/*
// ## REMOVE OTP 08Jul2021 ##
// Check OTP is correct
const checkOtpIsCorrect = await checkOtp(otp.otpToken, otp.otpToken, otp.refCode)
// ...
*/hr/hrAddress.js ปิด OTP ตั้งแต่ 8 Jul 2021 แต่ hr/hrPhoneNumber.js ยังทำงาน — sibling resolvers ใช้ pattern ต่างกัน (detail ใน 3.2.6)
QUAL-CU-01 · requires directive arg commented out
// if (requires) {
// if (!hasRole(requires, profile.profile.roles)) {
// // log.warning(...)
// throw new AuthenticationError('NOT_AUTHORIZED')
// }
// }RBAC role check ที่ประกาศใน schema (requires: [String]) ไม่ถูก implement — ทุก field ที่ใช้ @auth(requires: [...]) จะผ่านเสมอ (detail ใน 3.2.4)
🟢 Medium
QUAL-CU-02 · clockTolerance ใน JWT payload (misuse)
export const generateToken = (data, option = {}) => {
let dataOption = {
clockTolerance: 5
}
if (Object.keys(option).length > 0) {
dataOption = { ...dataOption, ...option }
}
data = { ...data, ...dataOption }clockTolerance เป็น jwt.verify option — ใส่เป็น payload claim ไม่มีผล เป็น misuse ของ jsonwebtoken API
QUAL-CU-03 · signInDomains hardcoded
const getOrgUnitLevel1 = () => {
return [
{ id: '...', value: '...', prefix: '@', textTh: '...', textEn: '...' },
// ... 26 rows total
]
}26 rows ของ org list hardcoded ใน code — เพิ่ม/แก้ org ต้อง deploy code ใหม่ ควรย้ายไป masterData table
OBS-CU-01 · dbHRMI_test dead connection
const dbHRMI_test = new Sequelize("dbHRMI_test", DB_USERNAME, DB_PASSWORD, {
// ...
pool: {
max: 100,
// ...
}
})connection pool ที่เปิดขึ้นทุก boot แต่ไม่มี model ผูก — เปลือง MSSQL resource 100 session (detail ใน 3.2.2)
PERF-CU-01 · ไม่มี timeout/retry/circuit breaker
ไม่มี centralized timeout config — แต่ละ integration ตั้ง timeout เอง (หรือไม่ตั้งเลย) detail ใน 3.2.7
🔴 ถ้า upstream service down บางตัว (เช่น SSO, HIS) request อาจ hang ไม่มี timeout
CORR-CU-03 · syncModelToTable ใน prod
db
.authenticate()
.then(async () => {
console.log('[MAIN] ✅ Connection has been established successfully (db).')
// Sync model to table
syncModelToTable()
})auto-sync ใน prod เป็น risk — model change deploy โดย accident จะ alter prod schema โดยไม่มี migration review (detail ใน 3.2.2)
Fix: gate ด้วย IS_PRODUCTION
Audit cross-reference
mapping กับ docs-v1-archive/audit/05-tech-debt-register.md
| Debt ID (v1) | Chapter | Topic |
|---|---|---|
| SEC-7 | LEG-CU-01, CORR-CU-01 | axois typosquat + httpRequest.js broken |
| SEC-8 | SEC-CU-03 | .env.develop-test committed |
| SEC-9 | LEG-CU-02 | error-ex git URL |
| LEG-1 | LEG-CU-05 | Node 12 EOL |
| LEG-2 | LEG-CU-05 | esm shim lock-in |
| LEG-3 | (Volume 3.1) | apollo-server v2 EOL |
| LEG-8 | LEG-CU-03 | ldapjs unmaintained |
| LEG-9 | LEG-CU-04 | jsonwebtoken v8 CVE |
| LEG-27 | LEG-CU-06 | xlsx prototype pollution |
| LEG-28 | — | sha512crypt-node (ใช้ใน activeDirectory.js) |
| CORR-9 | CORR-CU-01 | httpRequest.js broken |
Comparison กับ vtrc-api security
| Aspect | vtrc-api | cu-central-api |
|---|---|---|
| JWT secret default | 'secret' | '' (empty) |
| apiKeys | ใน config.js (7 prod) | hardcoded 2 keys ใน controller |
| Password handling | forward plaintext ไป centralize | verify ผ่าน LDAP โดยตรง |
execSync injection | 🔴 5 endpoints | 🟢 ไม่มี |
| SQL injection | 🔴 statLog.js | 🟢 ไม่พบ |
| CORS bug | 🔴 corss typo | 🟢 ไม่มี (ไม่ได้ set CORS) |
| introspection prod | 🔴 true เสมอ | 🟢 gate ด้วย IS_PRODUCTION |
| File upload | 🔴 multer no filter | 🟢 ไม่มี file upload endpoint |
| Rate limit | 🔴 ไม่มี | 🔴 ไม่มี |
| Passwordless signin | 🟢 ไม่มี | 🔴 /auth/signinbypass |
| LDAP TLS | n/a | 🔴 rejectUnauthorized: false |
cu-central-api มี security posture ที่ดีกว่า vtrc-api ในหลายด้าน (ไม่มี execSync, ไม่มี SQLi, gate introspection ถูก) แต่แย่กว่าในเรื่อง LDAP TLS และ ignoreAuth bypass
Modernization path
ตาม audit/05-tech-debt-register.md §5.3 cu-central-api ทั้งหมดควร decompose เป็น 2 Go services
cu-central-api (Node 12)
│
▼
┌──────────────────┐ ┌──────────────────┐
│ identity-service │ │ hr-data-service │
│ (Go + Hex/DDD) │ │ (Go + Hex/DDD) │
└──────────────────┘ └──────────────────┘
│ │
│ ├──▶ MSSQL (per-shard connection)
│ ├──▶ HRMI HTTP API
│ └──▶ HIS API
│
├──▶ LDAP/AD (go-ldap)
├──▶ Redis (go-redis)
├──▶ JWT (golang-jwt/jwt/v5)
└──▶ Casbin (RBAC policy)| Context | เหตุผลที่แยก |
|---|---|
identity-service | auth + RBAC + session เป็นส่วนงานที่ใช้ร่วมกันทุก service — ควรแยกเป็น service เดียวที่ทุกคนเรียก |
hr-data-service | การอ่าน/เขียน HR data เป็นงานเฉพาะตัว — มี SourceDB sharding เป็นรายละเอียดภายใน |
รายละเอียดเพิ่มเติมใน Volume 19 Roadmap
Wave 0 stop-the-bleed checklist
| # | Action | Severity | ID | Effort |
|---|---|---|---|---|
| 1 | Rotate JWT_SECRET + hard-fail at boot ถ้า empty | 🔴 | SEC-CU-01 | 0.5d |
| 2 | Rotate hardcoded apiKeys + ย้ายไป env | 🔴 | SEC-CU-02 | 1d |
| 3 | Purge .env.develop-test + rotate secrets | 🔴 | SEC-CU-03 | 1-2d |
| 4 | ลบ axois typosquat + add axios@^1.x + แก้ httpRequest.js import | 🔴 | LEG-CU-01, CORR-CU-01 | 0.5d |
| 5 | ลบ/กั้น /auth/signinbypass ใน prod | 🔴 | SEC-CU-04 | 0.5d |
| 6 | ลบ ignoreAuth bypass | 🔴 | SEC-CU-05 | 1d |
| 7 | Fix rejectUnauthorized: false ใน LDAP | 🔴 | SEC-CU-06 | 1d |
รวม Wave 0 ≈ 5-7 วัน — ลด Critical ทั้งหมด
อ่านต่อ → 3.2.9 Scorecard + comparison