Skip to content

3.3.8 · Security debt — full catalog

บทนี้รวบรวม vulnerability + code-quality issue ทั้งหมดที่พบใน centralize-api จากการ deep code dive ในบท 3.3.1–3.3.7 — จัดเรียงตาม severity (Critical → High → Medium → Low) พร้อมรายละเอียด code evidence, ผลกระทบ, และวิธีแก้


Severity definitions

Severityเกณฑ์SLA ที่แนะนำ
Criticalรั่วข้อมูล PII, auth bypass, รหัสผ่านรั่ว, RCEภายใน 24 ชม.
Highconfiguration ที่ทำให้ attack surface กว้าง, data integrity riskภายใน 1 สัปดาห์
Mediumcode quality issue ที่เพิ่ม risk ในอนาคต, ขาด defense-in-depthภายใน 1 เดือน
Lowmaintainability, naming, dead codebacklog

ภาพรวมจำนวน issue

SeverityCount
Critical4
High7
Medium6
Low5
Total22

Critical

CRIT-01 — ไฟล์ .env commit ลง git พร้อม credentials

centralize-api/.env:1-30 (ทบทวนจากบท 3.3.2)

ไฟล์ .env commit ลง repository มีข้อมูล

  • MSSQL credentials (DB_USER, DB_PASS)
  • JWT secret key (SECRET_KEY)
  • Internal IPs (10.188.128.160)
  • URL ของ related services

ผลกระทบ — ทุกคนที่เข้าถึง repo (รวมถึงอดีต contributor ที่ออกจากองค์กร) เห็น production credentials ทั้งหมด

การแก้

  1. ลบ .env ออกจาก git history (git filter-repo หรือ BFG)
  2. Rotate ทุก secret (MSSQL password, JWT secret)
  3. ใช้ secret manager (Vault, AWS Secrets Manager, GCP Secret Manager)
  4. Commit .env.example ที่ไม่มีค่าจริงแทน
  5. เพิ่ม .env เข้า .gitignore

CRIT-02 — Dockerfile คัดลอก .env เข้า image

centralize-api/Dockerfile (ทบทวนจากบท 3.3.2)

dockerfile
COPY .env ./

ถึงแม้จะลบ .env ออกจาก git (CRIT-01) — Dockerfile ยังคัดลอก .env เข้า image ในขั้นตอน build → ทุก image ที่ build จะมี secret ฝังอยู่

ผลกระทบ — ใครก็ตามที่ pull image (เช่นจาก registry) สามารถ docker history หรือ docker exec เข้าไปดู .env ได้

การแก้

  • ลบบรรทัด COPY .env ./
  • Inject secret ผ่าน docker run --env-file หรือ Kubernetes Secret หรือ runtime env var
  • ใช้ multi-stage build ที่ stage final ไม่มี .env

CRIT-03 — JWT secret ผิด — ใช้ค่าที่ generate สั้น ๆ ไม่ใช่ secret ที่ปลอดภัย

centralize-api/.env:SECRET_KEY

SECRET_KEY=127.0.0.1

(ทบทวนจากบท 3.3.5)

SECRET_KEY ที่ใช้ sign JWT เป็น string "127.0.0.1" — ค่าสั้น ทายง่าย และเป็น string ที่คาดเดาได้

ผลกระทบ — ผู้ไม่ประสงค์ดีที่รู้ค่า secret (เห็นได้จาก .env ที่ commit — CRIT-01) สามารถ forge JWT ของ user ใดก็ตามได้ → เข้าถึง centralize-api ในนาม user นั้น ๆ

การแก้ — ใช้ secret ที่ยาวอย่างน้อย 256 bit สุ่มจาก CSPRNG

bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

หรือใช้ RSA key pair (RS256 algorithm) แทน HMAC

CRIT-04 — @Public() decorator บนทุก route → auth bypass ทั้งระบบ

(ทบทวนจากบท 3.3.3, 3.3.5)

ทุก endpoint ใน centralize-api มี @Public() decorator

typescript
@Public()  // ← บนทุก endpoint
@Get('employee')
getEmployees(...) { ... }

JwtAuthGuard.canActivate (centralize-api/libs/jwt/jwt-auth.guard.ts:9-14) เช็คเงื่อนไขนี้

typescript
if (isPublic) {
  return true;
}

→ ทุก request ผ่านเข้าได้โดยไม่ต้อง validate JWT → auth bypass ทั้งระบบ

ผลกระทบ — ผู้ไม่ประสงค์ดีที่รู้ URL ของ centralize-api สามารถเรียกทุก endpoint ได้โดยไม่ต้อง authenticate รวมถึง endpoint ที่ return PII (empCode, fullName, bankAccount, positionName)

การแก้

  1. ลบ @Public() ออกจาก endpoint ที่ควรจะ protected (ทุก endpoint ยกเว้น /api/health ถ้ามี)
  2. เพิ่ม @UseGuards(JwtAuthGuard) ที่ controller level
  3. Validate JWT ใน request และเก็บ user object ใน req.user
  4. Implement role-based authorization ถ้าต้องการ

High

HIGH-01 — CORS origin: '*' รับทุก origin

(ทบทวนจากบท 3.3.7)

centralize-api/src/main.ts:31-35

typescript
app.enableCors({
  origin: '*',
  allowedHeaders: '*',
  methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS',
});

ผลกระทบ — script จาก domain ใดก็ตามใน browser ของเหยื่อสามารถเรียก centralize-api ได้ (CORS ป้องกัน browser-side เท่านั้น) → ร่วมกับ CRIT-04 (auth bypass) ทำให้ cross-site data exfiltration ทำได้ง่าย

การแก้ — whitelist origin ของ trusted frontends

typescript
origin: process.env.NODE_ENV === 'production'
  ? ['https://vtrc.redcross.or.th', 'https://vtrcbackoffice.redcross.or.th']
  : true

HIGH-02 — JwtStrategy.validate ไม่ validate อะไร — เก็บแค่ payload

centralize-api/libs/jwt/jwt.strategy.ts:14-19

typescript
async validate(payload: any) {
  console.log(payload);
  this.data = payload;
  return { userId: payload.sub, username: payload.username };
}

(ทบทวนจากบท 3.3.5)

ปัญหาย่อย 4 ข้อ

ปัญหารายละเอียด
ไม่ lookup user ใน DBเชื่อ payload ทันทีโดยไม่เช็คว่า user ยังมีอยู่จริง
console.log(payload)leak JWT payload ลง stdout (อาจรวม PII)
this.data = payloadเขียน state ลง instance field → thread-unsafe ใน concurrent request
return type เป็น anyTypeScript ไม่ช่วย type-check

การแก้

typescript
async validate(payload: JwtPayload): Promise<AuthenticatedUser> {
  const user = await this.usersService.findById(payload.sub);
  if (!user || user.revokedAt) {
    throw new UnauthorizedException();
  }
  return { userId: user.id, username: user.username, roles: user.roles };
}

HIGH-03 — JwtAuthGuard ไม่ catch invalid token error

centralize-api/libs/jwt/jwt-auth.guard.ts:16-18

typescript
handleRequest(err, user, info) {
  return user;
}

(ทบทวนจากบท 3.3.5)

handleRequest ละเว้น err และ info → แม้ว่า JWT จะหมดอายุ (info.name === 'jwt expired') หรือ malformed ก็ไม่ throw แค่ return undefined ออกไป → ต่อมา controller อาจเรียก req.user.userId → 500 error

การแก้

typescript
handleRequest(err, user, info) {
  if (err || info) {
    throw err || new UnauthorizedException(info.message);
  }
  return user;
}

HIGH-04 — PII รั่วลง log file ผ่าน TypeORM logging: ['query']

(ทบทวนจากบท 3.3.2, 3.3.7)

centralize-api/src/app.module.ts:31,50

typescript
TypeOrmModule.forRootAsync({
  name: 'center',
  // ...
  logging: ['query'],  // ← log ทุก SQL query พร้อม parameter
})

ผลกระทบ — ทุก query ที่รัน รวมพารามิเตอร์ (เช่น WHERE empCode = @0, ค่าที่ bind) ถูกเขียนลง log file รวม PII เช่น

  • empCode
  • firstName, lastName
  • bankAccount
  • idCard (จาก SQL บาง query)

→ ถ้า log file รั่ว (เช่น log aggregator แชร์กับทีมอื่น) ทำให้ PII รั่ว

การแก้

  • เปลี่ยนเป็น logging: ['error'] (log เฉพาะ error)
  • หรือใช้ custom logger ที่ redact PII pattern (\d{13}, \d{10}) ก่อนเขียน

HIGH-05 — Hardcoded internal IP ใน code + doc

centralize-api/.env:SERVER_URL, centralize-api/docs/document.config.ts:7

typescript
.addServer(`http://10.188.128.160:${process.env.PORT || 3000}/`)

(ทบทวนจากบท 3.3.7)

IP 10.188.128.160 ปรากฏใน .env และ Swagger addServer → เปิดเผย network topology ภายใน

ผลกระทบ

  • ถ้า Swagger doc รั่วออก internet (เช่น https://centralize.redcross.or.th/docs) → ผู้ไม่ประสงค์ดีรู้ internal IP เพื่อใช้ในการโจมตีอื่น ๆ (lateral movement)
  • ถ้า IP เปลี่ยน (เช่น migrate network) → ต้องแก้ code + rebuild image

การแก้ — ดึงจาก env var

typescript
.addServer(process.env.SERVER_URL || 'http://localhost:3000/')

HIGH-06 — Variable name หยาบคาย — fuckQuery

centralize-api/src/modules/employee/employee.service.ts:112

typescript
const fuckQuery = `
  WITH orgCTE AS (...)`;

(ทบทวนจากบท 3.3.4)

ผลกระทบ

  • ปรากฏใน codebase → ทำให้ professional environment ดูไม่เป็นมืออาชีพ
  • ถ้า string นี้รั่วออก client (เช่น stack trace) → damage reputation
  • ละเมิด code of conduct ของหลายองค์กร

การแก้ — rename เป็น orgCteQuery หรือ mainQuery

HIGH-07 — trustServerCertificate: true ปิด TLS verification ของ MSSQL

centralize-api/src/app.module.ts (ทบทวนจากบท 3.3.2)

typescript
extra: {
  trustServerCertificate: true,
}

ผลกระทบ — การเชื่อมต่อระหว่าง centralize-api → MSSQL ไม่ verify TLS certificate → MITM สามารถ intercept query และ response ได้ (รวม PII)

การแก้

  • ใน dev ใช้ self-signed cert ที่ trust เฉพาะ dev environment
  • ใน production ติดตั้ง CA cert ที่ถูกต้องทั้ง server และ client แล้วตั้ง trustServerCertificate: false

Medium

MED-01 — ไม่มี rate limiting

centralize-api ไม่มี rate limiting middleware → ผู้ไม่ประสงค์ดีสามารถยิงแบบ brute-force หรือ DoS ได้ง่าย

การแก้ — @nestjs/throttler

typescript
ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]),

แล้วใส่ @Throttle() บน controller

MED-02 — Helmet ไม่ได้ใช้

(ทบทวนจากบท 3.3.7)

ไม่มี helmet middleware → ไม่มี security headers (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security)

ผลกระทบ — clickjacking, MIME sniffing, downgrade attack เพิ่มขึ้น

การแก้

typescript
import helmet from 'helmet';
app.use(helmet());

MED-03 — ไม่มี health endpoint

(ทบทวนจากบท 3.3.7)

Load balancer/Kubernetes probe ต้องเรียก /api ที่รัน CTE 130 บรรทัด + cross-database join → probe ช้า + เปลือง resource

การแก้ — /health/live + /health/ready (ทบทวนตัวอย่างในบท 3.3.7)

MED-04 — Entity ไม่ครอบ table ทั้งหมด → drift risk

(ทบทวนจากบท 3.3.6)

มีเพียง 3 entity (จาก 16 tables/views ที่ใช้) → ส่วนใหญ่ใช้ raw SQL → ถ้า HRMI schema เปลี่ยน (column rename, type change) raw SQL จะ fail โดยไม่มี compile-time check

การแก้

  • สร้าง entity สำหรับทุก table ที่ใช้บ่อย
  • หรือสร้าง migration script ที่ validate raw SQL กับ schema HRMI

MED-05 — TypeScript strictNullChecks / noImplicitAny ไม่เปิด

centralize-api/tsconfig.json (ทบทวนจากบท 3.3.1)

json
{
  "compilerOptions": {
    "strictNullChecks": false,
    "noImplicitAny": false
  }
}

ผลกระทบ — TypeScript ไม่ช่วย catch null/undefined bug หรือ implicit any → runtime error ที่ป้องกันได้

การแก้

json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

MED-06 — Dependencies หลายตัว outdated

(ทบทวนจากบท 3.3.1)

centralize-api/package.json มี dependency ที่หลายเวอร์ชั่นเก่า เช่น

  • @nestjs/common@^8 (ปัจจุบัน v10)
  • moment@^2.29.1 (deprecated ในมาตรฐานใหม่)
  • class-validator@^0.13 (v0.14 มี patch security)

ผลกระทบ — ไม่ได้ security patch + ไม่ได้ bug fix + ใช้ API เก่าที่อาจ deprecate ในอนาคต

การแก้ — yarn upgrade-interactive --latest + ทดสอบ regression


Low

LOW-01 — UserEntity dead code

centralize-api/src/entities/user.entity.ts (ทบทวนจากบท 3.3.1, 3.3.6)

Entity UserEntity (table auth_user) เป็น dead code — copy มาจาก Postgres starter template ไม่ได้ใช้ → ลบทิ้ง

LOW-02 — EmployeeResponseDto dead code

centralize-api/src/dto/employee.dto.ts (ทบทวนจากบท 3.3.3)

DTO ที่ไม่ได้ใช้ → ลบทิ้ง

LOW-03 — Profanity (ซ้ำกับ HIGH-06)

centralize-api/src/modules/employee/employee.service.ts:112 มี fuckQuery → รวมไว้ใน HIGH-06 เพราะ impact สูงกว่า maintainability

LOW-04 — CTE copy-paste ~80 บรรทัด ใน service

centralize-api/src/modules/employee/employee.service.ts (ทบทวนจากบท 3.3.4)

getOrganizations, getDivisions, getDepartments, getManagers ใช้ CTE ที่เหมือนกัน ~80 บรรทัดใน 4 methods → DRY violation

การแก้ — สกัด CTE เป็น helper function หรือ view ใน HRMI

LOW-05 — console.log ใน production code

centralize-api/libs/jwt/jwt.strategy.ts:15

typescript
console.log(payload);

(ทบทวนจากบท 3.3.5, HIGH-02)

console.log ใน code path ที่ production ใช้ → leak ข้อมูล และทำให้ logger structure ไม่ consistent

การแก้ — ใช้ I3GatewayLogger ที่ inject ผ่าน DI


ตารารวม

IDSeverityTitleChapterFile:Line
CRIT-01Critical.env commit ลง git3.3.2centralize-api/.env
CRIT-02CriticalDockerfile คัดลอก .env3.3.2centralize-api/Dockerfile
CRIT-03CriticalJWT secret ผิด (127.0.0.1)3.3.5centralize-api/.env:SECRET_KEY
CRIT-04Critical@Public() บนทุก route → auth bypass3.3.3, 3.3.5centralize-api/libs/jwt/jwt-auth.guard.ts:9-14
HIGH-01HighCORS origin: '*'3.3.7centralize-api/src/main.ts:31-35
HIGH-02HighJwtStrategy.validate ไม่ validate3.3.5centralize-api/libs/jwt/jwt.strategy.ts:14-19
HIGH-03HighhandleRequest ไม่ catch error3.3.5centralize-api/libs/jwt/jwt-auth.guard.ts:16-18
HIGH-04HighTypeORM logging: ['query'] → PII log3.3.2, 3.3.7centralize-api/src/app.module.ts:31,50
HIGH-05HighHardcoded internal IP3.3.7centralize-api/docs/document.config.ts:7
HIGH-06HighfuckQuery profanity3.3.4centralize-api/src/modules/employee/employee.service.ts:112
HIGH-07HightrustServerCertificate: true3.3.2centralize-api/src/app.module.ts
MED-01Mediumไม่มี rate limiting3.3.7(global)
MED-02Mediumไม่มี helmet3.3.7(global)
MED-03Mediumไม่มี health endpoint3.3.7(global)
MED-04MediumEntity ไม่ครอบ table ทั้งหมด3.3.6centralize-api/src/entities/
MED-05MediumTypeScript strict mode ปิด3.3.1centralize-api/tsconfig.json
MED-06MediumDependencies outdated3.3.1centralize-api/package.json
LOW-01LowUserEntity dead code3.3.1, 3.3.6centralize-api/src/entities/user.entity.ts
LOW-02LowEmployeeResponseDto dead code3.3.3centralize-api/src/dto/employee.dto.ts
LOW-03LowProfanity (รวมใน HIGH-06)3.3.4centralize-api/src/modules/employee/employee.service.ts:112
LOW-04LowCTE copy-paste ~80 บรรทัด3.3.4centralize-api/src/modules/employee/employee.service.ts
LOW-05Lowconsole.log ใน production3.3.5centralize-api/libs/jwt/jwt.strategy.ts:15

Priority การแก้ — ลำดับที่แนะนำ

PhaseItemsRationale
Phase 1 — ภายใน 1 สัปดาห์CRIT-01, CRIT-02, CRIT-03, CRIT-04หยุด credential leak + ปิด auth bypass
Phase 2 — ภายใน 1 เดือนHIGH-01 → HIGH-07ลด attack surface + ป้องกัน PII leak
Phase 3 — backlogMED-01 → MED-06, LOW-01 → LOW-05Code quality + maintainability

Phase 1 — minimal fix

ถ้าทีมมีเวลาจำกัด แนะนำขั้นต่ำ

  1. ลบ .env ออกจาก git + rotate secret (CRIT-01, CRIT-03)
  2. ลบ COPY .env ./ จาก Dockerfile (CRIT-02)
  3. ลบ @Public() ออกจาก endpoint ที่ไม่จำเป็น (CRIT-04) — ขั้นต่ำคือ endpoint ที่ return PII (/employee/*, /hrTimeTemp/*, /hrPaySlip/*)

ขั้นต่ำ 3 ข้อนี้ยก threat level ลงได้มาก


ข้อสังเกตเกี่ยวกับ security model ของ centralize-api

centralize-api ไม่ได้ออกแบบให้เป็น internet-facing — ทุกการอ้างอิงบ่งชี้ว่าเป็น internal-only service

  • Internal IP hardcoded (10.188.128.160)
  • Trust network implicitly (trustServerCertificate: true, origin: '*')
  • @Public() บนทุก route — เพราะ trust network layer

→ ปัญหาทั้งหมดในบทนี้จะ "less critical" ถ้า centralize-api อยู่หลัง firewall + VPN เท่านั้น และ caller (vtrc-api) เป็น trusted service

แต่ — ถ้ามีแผนจะ expose centralize-api ออก internet (หรือผ่าน API gateway ที่ไม่ enforce auth) → ทุก Critical/High ต้องแก้ก่อน


สรุปประเด็นสำคัญ

  • 22 issue รวม — 4 Critical, 7 High, 6 Medium, 5 Low
  • Auth bypass เป็น root cause ของความเสี่ยงส่วนใหญ่ — @Public() บนทุก route + JWT secret ที่ทายง่าย
  • PII leak ผ่านหลายทาง — log file (logging: ['query']), console.log, hardcoded IP ใน doc
  • Trust model ผิดปกติ — service ออกแบบเหมือนเป็น internal-only แต่ไม่มี document ชัดเจน
  • Fix Phase 1 ขั้นต่ำ 3 ข้อ ลด critical risk ได้มาก → ลบ .env, ลบ COPY .env, ลบ @Public() บน endpoint ที่ return PII
  • Dependency outdated + TypeScript strict ปิด เพิ่ม risk ในอนาคต