Skip to content

3.2.8 · Security + debt

บทนี้ catalog ความเสี่ยงด้านความปลอดภัยและ technical debt ของ cu-central-api โดยเน้นสิ่งที่ต่างจาก vtrc-api (ส่วนที่เหมือนกัน cover ใน Volume 12 Security แล้ว)


Severity summary

SeverityCount
🔴 Critical6
🟡 High7
🟢 Medium5

🔴 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

javascript
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 หา axios package ที่ไม่มี)
  • แต่ 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 ทันทีเพราะ axios module ไม่ถูกประกาศ

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)ChapterTopic
SEC-7LEG-CU-01, CORR-CU-01axois typosquat + httpRequest.js broken
SEC-8SEC-CU-03.env.develop-test committed
SEC-9LEG-CU-02error-ex git URL
LEG-1LEG-CU-05Node 12 EOL
LEG-2LEG-CU-05esm shim lock-in
LEG-3(Volume 3.1)apollo-server v2 EOL
LEG-8LEG-CU-03ldapjs unmaintained
LEG-9LEG-CU-04jsonwebtoken v8 CVE
LEG-27LEG-CU-06xlsx prototype pollution
LEG-28sha512crypt-node (ใช้ใน activeDirectory.js)
CORR-9CORR-CU-01httpRequest.js broken

Comparison กับ vtrc-api security

Aspectvtrc-apicu-central-api
JWT secret default'secret''' (empty)
apiKeysใน config.js (7 prod)hardcoded 2 keys ใน controller
Password handlingforward plaintext ไป centralizeverify ผ่าน 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 TLSn/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-serviceauth + RBAC + session เป็นส่วนงานที่ใช้ร่วมกันทุก service — ควรแยกเป็น service เดียวที่ทุกคนเรียก
hr-data-serviceการอ่าน/เขียน HR data เป็นงานเฉพาะตัว — มี SourceDB sharding เป็นรายละเอียดภายใน

รายละเอียดเพิ่มเติมใน Volume 19 Roadmap


Wave 0 stop-the-bleed checklist

#ActionSeverityIDEffort
1Rotate JWT_SECRET + hard-fail at boot ถ้า empty🔴SEC-CU-010.5d
2Rotate hardcoded apiKeys + ย้ายไป env🔴SEC-CU-021d
3Purge .env.develop-test + rotate secrets🔴SEC-CU-031-2d
4ลบ axois typosquat + add axios@^1.x + แก้ httpRequest.js import🔴LEG-CU-01, CORR-CU-010.5d
5ลบ/กั้น /auth/signinbypass ใน prod🔴SEC-CU-040.5d
6ลบ ignoreAuth bypass🔴SEC-CU-051d
7Fix rejectUnauthorized: false ใน LDAP🔴SEC-CU-061d

รวม Wave 0 ≈ 5-7 วัน — ลด Critical ทั้งหมด


อ่านต่อ → 3.2.9 Scorecard + comparison