Skip to content

4 · Scorecard — chat-center-api

บทสรุป debt จาก 0103 — severity catalog แบบเจาะหลักฐาน + remediation

อ้างอิง branch: prod


Severity legend

  • 🔴 Critical = active security/data-loss/availability risk
  • 🟡 High = correctness/maintainability hazard, real impact
  • 🟢 Medium = friction/hygiene, not yet dangerous
Levelเกณฑ์Action
🔴 Criticalactive security/data-loss/availability riskต้องแก้ก่อน deploy ถัดไป
🟡 Highcorrectness/maintainability hazardวางแผนแก้ใน quarter นี้
🟢 Mediumfriction/hygieneแก้ตามโอกาส

Severity: Critical (🔴)

SEC-CHAT-01 · Raw SQL string interpolation — SQL injection

ที่:

  • chat-center-api/src/modules/chat/chat.service.ts (branch: prod):103-157rooms()
  • chat-center-api/src/modules/chat/chat.service.ts (branch: prod):217-260chatRoomHistory() (interpolate sourceDB + tags จากทีม)
  • chat-center-api/src/modules/tags/tags.service.ts (branch: prod):55-61setting()
  • chat-center-api/src/app.service.ts (branch: prod):277-281imageProfileMulti interpolate sourceDB

Evidencerooms():

typescript
let queryString = `SELECT ... FROM chatRoom where sourceDB = '${user.profile.sourceDB}' `;
if (query.fullName) queryString += ` and fullName like '%${query.fullName}%'  `;
if (query.diviCode) queryString += ` and diviCode = '${query.diviCode}' `;
if (query.status) queryString += ` and status = '${query.status}'  `;
if (query.tagsId) queryString += ` and tagsId like '%"${query.tagsId}"%'   `;
const rooms = await this.entityManager.query(queryString);

ค่า fullName / diviCode / status / tagsId มาจาก HTTP query (FilterRoomDTO) โดยไม่ bind parameter

Impact: authenticated user ที่ได้ JWT แล้วสามารถ inject เข้า connection main (database 2way) — อ่าน/แก้ข้อมูลห้องแชท, tags, notifications ของ tenant ตามสิทธิ์ DB account

การแก้ไข:

  1. เปลี่ยนเป็น parameterized: entityManager.query('... WHERE sourceDB = @0 AND fullName LIKE @1', [sourceDB, '%' + fullName + '%'])
  2. หรือ TypeORM QueryBuilder .where('fullName LIKE :fn', { fn: ... })
  3. Sweep จุดที่เหลือใน chatRoomHistory, tags.setting, imageProfileMulti

เปรียบเทียบจุดที่ทำถูกแล้วใน repo เดียวกัน: userLine lookup ใช้ @0 (app.service.ts:35, chat.gateway.ts:69) และ listNoti ใช้ QueryBuilder named params (chat.service.ts:299-303)


SEC-CHAT-04 · Production/UAT credentials committed ใน git

ที่:

  • chat-center-api/.env (branch: prod) — ทั้งไฟล์
  • chat-center-api/.env.prod, .env.uat — สำเนา credential

Evidence: ไฟล์มีอย่างน้อย DB_PASSWORD, DB_HRMI_PASSWORD, SECRET_KEY, MONGODB URI (มี user/password), LINE_CHANNEL_SECRET, LINE_AUTH — และถูก track ใน git (ไม่ได้อยู่ใน ignore ที่ได้ผลสำหรับไฟล์เหล่านี้)

.gitignore มีอยู่แต่ .env ยังถูก commit แล้วใน history

Impact:

  • เข้า MSSQL 2way + dbHRMI_Center ได้ถ้า network เปิดถึง 10.188.128.61
  • Forge JWT ด้วย SECRET_KEY → เรียก REST API ในนามพนักงานใดก็ได้
  • ใช้ LINE channel token ส่งข้อความในนาม Official Account
  • อ่าน/เขียน MongoDB chatDB

การแก้ไข:

  1. Rotate credential ทั้งหมด (DB ×2, Mongo, JWT, LINE)
  2. ลบ .env* ออกจาก tracking + purge history (git filter-repo)
  3. ย้ายไป secret manager / k8s Secret
  4. ยืนยันว่า .gitignore ครอบคลุมและ pre-commit กันไม่ให้ commit ซ้ำ

Severity: High (🟡)

SEC-CHAT-02 · WebSocket ไม่มี auth + เชื่อ fromline จาก client

ที่: chat-center-api/src/modules/chat/chat.gateway.ts (branch: prod):19-26,49-66

Evidence:

typescript
@WebSocketGateway({
  cors: { origin: '*' },
  path: '/chat-center-ws',
  transports: ['websocket'],
  maxHttpBufferSize: 1e8
})
// ...
if (message?.fromline) {
  this.server.emit(message.replyToken, { data: message.message, type: 'text', sendBy: 'user' });
  return true;
}

Global JwtAuthGuard ไม่ครอบ WebSocket (main.ts:34 ผูก HTTP เท่านั้น)

Impact:

  • Client ใดก็ได้เชื่อม /chat-center-ws แล้ว emit chat เพื่อ push ข้อความไป LINE (branch แอดมิน) ถ้าเดา/รู้ lineToken
  • Client ปลอม fromline: true เพื่อ inject ข้อความเข้า UI ของแอดมินโดยไม่ผ่าน webhook

การแก้ไข:

  1. เพิ่ม WsGuard ตรวจ JWT ตอน handshake (เช่น auth.token ใน Socket.IO)
  2. เลิกเชื่อ fromline จาก client — ให้เฉพาะ AppService.actions (server-side) เรียก internal method ที่ไม่ expose ผ่าน @SubscribeMessage
  3. จำกัด CORS origin ให้เหลือ domain backoffice จริง

SEC-CHAT-03 · LINE webhook ไม่ verify signature

ที่: chat-center-api/src/app.controller.ts (branch: prod):18-22, app.service.ts:29-186

Evidence: @Public() + รับ body ตรง ๆ; lineChannelSecret ถูกโหลดใน config (configuration.ts:39) แต่ไม่พบการใช้ใน verify path

Impact: attacker ที่รู้ URL webhook สามารถ POST ปลอม events เพื่อสร้างห้อง, เปลี่ยน tags, หรือ inject conversation ได้ (ต้องมี userLine mapping ที่ตรงกับ source.userId ถึงจะเข้า flow เต็ม — แต่ยังเป็น attack surface ที่ไม่ควรว่าง)

การแก้ไข: ตรวจ HMAC-SHA256 ของ raw body ด้วย LINE_CHANNEL_SECRET ตาม LINE Messaging API — Signature validation ก่อนเรียก actions()


CORR-CHAT-01 · Appointment reminder เป็น in-memory CronJob

ที่: chat-center-api/src/modules/appointment/appointment.service.ts (branch: prod):33-67

Evidence: new CronJob(timeJob, async () => { ... }).start() หลัง save entity — ไม่มีตาราง job / ไม่ re-hydrate ตอน onModuleInit

updateAppointment แก้เฉพาะแถว DB ไม่แตะ timer เดิม (appointment.service.ts:74-86)

Impact: หลัง deploy/crash ก่อนเวลานัด — แจ้งเตือนเงียบหาย; แก้เวลานัดแล้ว reminder เก่ายังยิงตามเวลาเดิม (หรือหาย)

การแก้ไข: ย้ายไป @nestjs/schedule ที่ query appointment ที่ appointmentDate - timeDiff อยู่ในหน้าต่างเวลาสั้น ๆ ทุก N นาที + ตั้ง flag remindedAt กันยิงซ้ำ


CORR-CHAT-02 · synchronize: true บน connection main

ที่: chat-center-api/config/configuration.ts (branch: prod):14

Impact: TypeORM เปลี่ยน schema production ตาม entity ทุก boot — เสี่ยง drop/alter column โดยไม่ผ่าน review

การแก้ไข: synchronize: false + migration files ที่ review ได้


CORR-CHAT-03 · Feedback URL hardcode เป็น UAT

ที่:

  • chat-center-api/src/app.service.ts (branch: prod):242
  • chat-center-api/src/modules/satisfactionSurvey/satisfactionsurvey.service.ts (branch: prod):94

Evidence: ลิงก์ในข้อความ LINE / response ของ genSurvey ชี้

text
https://uat-vtrc.redcross.or.th/2way-backoffice/2wayfrontend/2waybackoffice/Feedback/{key}

ขณะที่ SERVER_URL ใน .env เป็น https://vtrcapi.redcross.or.th/

Impact: ผู้ใช้ที่คุยบนช่องทาง production อาจถูกส่งไปหน้าแบบสำรวจบน UAT (หรือกลับกันตามว่า API รัน env ไหน)

การแก้ไข: สร้าง config FEEDBACK_BASE_URL ต่อ environment แทน hardcode


CORR-CHAT-04 · ข้อความ notification ของ cron 24h ไม่ตรงบริบท

ที่: chat-center-api/src/modules/cronJobs/cronjobs.service.ts (branch: prod):63-74

Evidence: เมื่อห้องมี care taker และสถานะค้าง ≥24 ชม. ข้อความคือ `คุณมีการนัดหมายกับ ${roomDetail.fullName}` — copy จาก appointment flow ไม่ใช่ "รอตอบกลับ" (ข้อความฝั่งไม่มี care taker ใช้ "รอตอบกลับ" ถูกต้องกว่าที่บรรทัด 84)

Impact: แอดมินเข้าใจผิดว่ามีการนัดหมาย

การแก้ไข: แยกข้อความให้ตรงเหตุการณ์ (รอตอบกลับ ≠ นัดหมาย)


Severity: Medium (🟢)

QUAL-CHAT-01 · libs/ copy-paste ข้าม repo

libs/jwt, libs/log, libs/exception เหมือนกับ vtrc-common / job-scheduler-api แบบ copy — แก้บั๊ก guard ต้องไล่หลาย repo

การแก้ไข: publish shared package จริง (private npm) หรือ consume vtrc-common เป็น dependency


QUAL-CHAT-02 · console.log + dead commented code จำนวนมาก

ตัวอย่าง: chat.gateway.ts:59,73-74, chat.service.ts:38,54,100, app.service.ts:32,36 — log ทั้ง body ข้อความและ profile; บล็อก comment เรื่อง cu-Gateway ค้างยาว

การแก้ไข: ใช้ I3GatewayLogger ที่มีอยู่; ลบ dead code ที่ comment แล้ว


QUAL-CHAT-03 · Path / typo ค้างใน API surface

  • Controller path satisfacedurvey (สะกดผิด — ขาด t ใน satisfied) (satisfedurvey.controller.ts:14)
  • Method/report reportServey (survey สะกดผิด) (satisfedurvey.controller.ts:34-36)
  • package.json name rest-structure ไม่ตรงชื่อ service

Impact: client ต้อง mirror typo; documentation และ search ยาก


QUAL-CHAT-04 · ChatGateway ลงทะเบียนที่ AppModule ไม่ใช่ ChatModule

app.module.ts:58 vs chat.module.ts:20-21 — ทำให้ dependency graph สับสน (AppService ↔ ChatGateway ↔ Mongo models ที่ register ทั้ง root และ ChatModule)


OBS-CHAT-01 · ไม่มี deploy manifest ใน repo

ไม่มี docker-compose.yml, ไม่พบ CI config ใน tree ที่อ่านได้ — วิธี orchestrate production UNVERIFIED


OBS-CHAT-02 · Uploads อยู่บน local disk โดยไม่ยืนยัน volume mount

ไฟล์แชทอยู่ใน ./uploads/ ของ container (chat.gateway.ts:83-87) — ถ้าไม่มี persistent volume ไฟล์และลิงก์ที่ส่งไป LINE จะตายหลัง redeploy


OBS-CHAT-03 · Swagger เปิดในทุก environment + ngrok leftover

main.ts:44-45 setup Swagger ตลอด; document.config.ts:9 มี ngrok URL hardcode


Health scorecard summary

Debt IDSeverityหมวดสรุปสั้น
SEC-CHAT-01🔴 CriticalSecuritySQL injection ผ่าน query string ใน rooms / tags setting
SEC-CHAT-04🔴 CriticalSecurity.env credential หลุดใน git
SEC-CHAT-02🟡 HighSecurityWS ไม่มี auth + เชื่อ fromline
SEC-CHAT-03🟡 HighSecurityWebhook ไม่ verify LINE signature
CORR-CHAT-01🟡 HighCorrectnessAppointment CronJob หายเมื่อ restart
CORR-CHAT-02🟡 HighCorrectnesssynchronize: true บน main
CORR-CHAT-03🟡 HighCorrectnessFeedback URL hardcode UAT
CORR-CHAT-04🟡 HighCorrectnessข้อความ cron 24h ผิดบริบท
QUAL-CHAT-01🟢 MediumQualitylibs copy-paste
QUAL-CHAT-02🟢 MediumQualityconsole.log / dead code
QUAL-CHAT-03🟢 MediumQualitytypo ใน path API
QUAL-CHAT-04🟢 MediumQualityGateway ลงทะเบียนผิดชั้น module
OBS-CHAT-01🟢 MediumObservabilityไม่มี compose/CI ใน repo
OBS-CHAT-02🟢 MediumObservabilityuploads ไม่ยืนยัน persistence
OBS-CHAT-03🟢 MediumObservabilitySwagger + ngrok leftover

Modernization priority (แนะนำลำดับ)

  1. Rotate + ลบ secrets จาก git (SEC-CHAT-04)
  2. Parameterize SQL ทุกจุด (SEC-CHAT-01)
  3. Verify LINE webhook signature (SEC-CHAT-03)
  4. ใส่ auth บน Socket.IO + แยก internal emit จาก client event (SEC-CHAT-02)
  5. ปิด synchronize + ทำ migration (CORR-CHAT-02)
  6. ย้าย appointment reminder ไป durable scheduler (CORR-CHAT-01)
  7. Config-driven feedback URL (CORR-CHAT-03)

UNVERIFIED

  • วิธี orchestrate/deploy จริงใน production (ไม่มี compose/CI ใน repo) — OBS-CHAT-01
  • ตัวตนของ 20.212.116.206:8200 ใน comment (dead code) — ไม่ยืนยันว่าเป็น service ใดในแพลตฟอร์ม
  • ว่า nginx / API gateway ด้านหน้ามีการ verify LINE signature แทนแอปหรือไม่ — ไม่พบหลักฐานใน repo นี้; จากโค้ดแอปอย่างเดียวถือว่ายังไม่ verify
  • schema จริงของตาราง userLine (ไม่มี entity ใน repo) — รู้แค่คอลัมน์ที่ถูก select: มีอย่างน้อย lineID, userID