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 ชม. |
| High | configuration ที่ทำให้ attack surface กว้าง, data integrity risk | ภายใน 1 สัปดาห์ |
| Medium | code quality issue ที่เพิ่ม risk ในอนาคต, ขาด defense-in-depth | ภายใน 1 เดือน |
| Low | maintainability, naming, dead code | backlog |
ภาพรวมจำนวน issue
| Severity | Count |
|---|---|
| Critical | 4 |
| High | 7 |
| Medium | 6 |
| Low | 5 |
| Total | 22 |
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 ทั้งหมด
การแก้
- ลบ
.envออกจาก git history (git filter-repoหรือ BFG) - Rotate ทุก secret (MSSQL password, JWT secret)
- ใช้ secret manager (Vault, AWS Secrets Manager, GCP Secret Manager)
- Commit
.env.exampleที่ไม่มีค่าจริงแทน - เพิ่ม
.envเข้า.gitignore
CRIT-02 — Dockerfile คัดลอก .env เข้า image
centralize-api/Dockerfile (ทบทวนจากบท 3.3.2)
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
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
@Public() // ← บนทุก endpoint
@Get('employee')
getEmployees(...) { ... }JwtAuthGuard.canActivate (centralize-api/libs/jwt/jwt-auth.guard.ts:9-14) เช็คเงื่อนไขนี้
if (isPublic) {
return true;
}→ ทุก request ผ่านเข้าได้โดยไม่ต้อง validate JWT → auth bypass ทั้งระบบ
ผลกระทบ — ผู้ไม่ประสงค์ดีที่รู้ URL ของ centralize-api สามารถเรียกทุก endpoint ได้โดยไม่ต้อง authenticate รวมถึง endpoint ที่ return PII (empCode, fullName, bankAccount, positionName)
การแก้
- ลบ
@Public()ออกจาก endpoint ที่ควรจะ protected (ทุก endpoint ยกเว้น/api/healthถ้ามี) - เพิ่ม
@UseGuards(JwtAuthGuard)ที่ controller level - Validate JWT ใน request และเก็บ
userobject ในreq.user - Implement role-based authorization ถ้าต้องการ
High
HIGH-01 — CORS origin: '*' รับทุก origin
(ทบทวนจากบท 3.3.7)
centralize-api/src/main.ts:31-35
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
origin: process.env.NODE_ENV === 'production'
? ['https://vtrc.redcross.or.th', 'https://vtrcbackoffice.redcross.or.th']
: trueHIGH-02 — JwtStrategy.validate ไม่ validate อะไร — เก็บแค่ payload
centralize-api/libs/jwt/jwt.strategy.ts:14-19
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 เป็น any | TypeScript ไม่ช่วย type-check |
การแก้
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
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
การแก้
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
TypeOrmModule.forRootAsync({
name: 'center',
// ...
logging: ['query'], // ← log ทุก SQL query พร้อม parameter
})ผลกระทบ — ทุก query ที่รัน รวมพารามิเตอร์ (เช่น WHERE empCode = @0, ค่าที่ bind) ถูกเขียนลง log file รวม PII เช่น
empCodefirstName,lastNamebankAccountidCard(จาก 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
.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
.addServer(process.env.SERVER_URL || 'http://localhost:3000/')HIGH-06 — Variable name หยาบคาย — fuckQuery
centralize-api/src/modules/employee/employee.service.ts:112
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)
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
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 เพิ่มขึ้น
การแก้
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)
{
"compilerOptions": {
"strictNullChecks": false,
"noImplicitAny": false
}
}ผลกระทบ — TypeScript ไม่ช่วย catch null/undefined bug หรือ implicit any → runtime error ที่ป้องกันได้
การแก้
{
"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
console.log(payload);(ทบทวนจากบท 3.3.5, HIGH-02)
console.log ใน code path ที่ production ใช้ → leak ข้อมูล และทำให้ logger structure ไม่ consistent
การแก้ — ใช้ I3GatewayLogger ที่ inject ผ่าน DI
ตารารวม
| ID | Severity | Title | Chapter | File:Line |
|---|---|---|---|---|
| CRIT-01 | Critical | .env commit ลง git | 3.3.2 | centralize-api/.env |
| CRIT-02 | Critical | Dockerfile คัดลอก .env | 3.3.2 | centralize-api/Dockerfile |
| CRIT-03 | Critical | JWT secret ผิด (127.0.0.1) | 3.3.5 | centralize-api/.env:SECRET_KEY |
| CRIT-04 | Critical | @Public() บนทุก route → auth bypass | 3.3.3, 3.3.5 | centralize-api/libs/jwt/jwt-auth.guard.ts:9-14 |
| HIGH-01 | High | CORS origin: '*' | 3.3.7 | centralize-api/src/main.ts:31-35 |
| HIGH-02 | High | JwtStrategy.validate ไม่ validate | 3.3.5 | centralize-api/libs/jwt/jwt.strategy.ts:14-19 |
| HIGH-03 | High | handleRequest ไม่ catch error | 3.3.5 | centralize-api/libs/jwt/jwt-auth.guard.ts:16-18 |
| HIGH-04 | High | TypeORM logging: ['query'] → PII log | 3.3.2, 3.3.7 | centralize-api/src/app.module.ts:31,50 |
| HIGH-05 | High | Hardcoded internal IP | 3.3.7 | centralize-api/docs/document.config.ts:7 |
| HIGH-06 | High | fuckQuery profanity | 3.3.4 | centralize-api/src/modules/employee/employee.service.ts:112 |
| HIGH-07 | High | trustServerCertificate: true | 3.3.2 | centralize-api/src/app.module.ts |
| MED-01 | Medium | ไม่มี rate limiting | 3.3.7 | (global) |
| MED-02 | Medium | ไม่มี helmet | 3.3.7 | (global) |
| MED-03 | Medium | ไม่มี health endpoint | 3.3.7 | (global) |
| MED-04 | Medium | Entity ไม่ครอบ table ทั้งหมด | 3.3.6 | centralize-api/src/entities/ |
| MED-05 | Medium | TypeScript strict mode ปิด | 3.3.1 | centralize-api/tsconfig.json |
| MED-06 | Medium | Dependencies outdated | 3.3.1 | centralize-api/package.json |
| LOW-01 | Low | UserEntity dead code | 3.3.1, 3.3.6 | centralize-api/src/entities/user.entity.ts |
| LOW-02 | Low | EmployeeResponseDto dead code | 3.3.3 | centralize-api/src/dto/employee.dto.ts |
| LOW-03 | Low | Profanity (รวมใน HIGH-06) | 3.3.4 | centralize-api/src/modules/employee/employee.service.ts:112 |
| LOW-04 | Low | CTE copy-paste ~80 บรรทัด | 3.3.4 | centralize-api/src/modules/employee/employee.service.ts |
| LOW-05 | Low | console.log ใน production | 3.3.5 | centralize-api/libs/jwt/jwt.strategy.ts:15 |
Priority การแก้ — ลำดับที่แนะนำ
| Phase | Items | Rationale |
|---|---|---|
| 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 — backlog | MED-01 → MED-06, LOW-01 → LOW-05 | Code quality + maintainability |
Phase 1 — minimal fix
ถ้าทีมมีเวลาจำกัด แนะนำขั้นต่ำ
- ลบ
.envออกจาก git + rotate secret (CRIT-01, CRIT-03) - ลบ
COPY .env ./จาก Dockerfile (CRIT-02) - ลบ
@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 ในอนาคต