4 · Scorecard — chat-center-api
บทสรุป debt จาก 01–03 — 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 |
|---|---|---|
| 🔴 Critical | active security/data-loss/availability risk | ต้องแก้ก่อน deploy ถัดไป |
| 🟡 High | correctness/maintainability hazard | วางแผนแก้ใน quarter นี้ |
| 🟢 Medium | friction/hygiene | แก้ตามโอกาส |
Severity: Critical (🔴)
SEC-CHAT-01 · Raw SQL string interpolation — SQL injection
ที่:
chat-center-api/src/modules/chat/chat.service.ts (branch: prod):103-157—rooms()chat-center-api/src/modules/chat/chat.service.ts (branch: prod):217-260—chatRoomHistory()(interpolatesourceDB+ tags จากทีม)chat-center-api/src/modules/tags/tags.service.ts (branch: prod):55-61—setting()chat-center-api/src/app.service.ts (branch: prod):277-281—imageProfileMultiinterpolatesourceDB
Evidence — rooms():
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
การแก้ไข:
- เปลี่ยนเป็น parameterized:
entityManager.query('... WHERE sourceDB = @0 AND fullName LIKE @1', [sourceDB, '%' + fullName + '%']) - หรือ TypeORM QueryBuilder
.where('fullName LIKE :fn', { fn: ... }) - 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
การแก้ไข:
- Rotate credential ทั้งหมด (DB ×2, Mongo, JWT, LINE)
- ลบ
.env*ออกจาก tracking + purge history (git filter-repo) - ย้ายไป secret manager / k8s Secret
- ยืนยันว่า
.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:
@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แล้ว emitchatเพื่อ push ข้อความไป LINE (branch แอดมิน) ถ้าเดา/รู้lineToken - Client ปลอม
fromline: trueเพื่อ inject ข้อความเข้า UI ของแอดมินโดยไม่ผ่าน webhook
การแก้ไข:
- เพิ่ม WsGuard ตรวจ JWT ตอน handshake (เช่น
auth.tokenใน Socket.IO) - เลิกเชื่อ
fromlineจาก client — ให้เฉพาะAppService.actions(server-side) เรียก internal method ที่ไม่ expose ผ่าน@SubscribeMessage - จำกัด 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):242chat-center-api/src/modules/satisfactionSurvey/satisfactionsurvey.service.ts (branch: prod):94
Evidence: ลิงก์ในข้อความ LINE / response ของ genSurvey ชี้
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.jsonnamerest-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 ID | Severity | หมวด | สรุปสั้น |
|---|---|---|---|
| SEC-CHAT-01 | 🔴 Critical | Security | SQL injection ผ่าน query string ใน rooms / tags setting |
| SEC-CHAT-04 | 🔴 Critical | Security | .env credential หลุดใน git |
| SEC-CHAT-02 | 🟡 High | Security | WS ไม่มี auth + เชื่อ fromline |
| SEC-CHAT-03 | 🟡 High | Security | Webhook ไม่ verify LINE signature |
| CORR-CHAT-01 | 🟡 High | Correctness | Appointment CronJob หายเมื่อ restart |
| CORR-CHAT-02 | 🟡 High | Correctness | synchronize: true บน main |
| CORR-CHAT-03 | 🟡 High | Correctness | Feedback URL hardcode UAT |
| CORR-CHAT-04 | 🟡 High | Correctness | ข้อความ cron 24h ผิดบริบท |
| QUAL-CHAT-01 | 🟢 Medium | Quality | libs copy-paste |
| QUAL-CHAT-02 | 🟢 Medium | Quality | console.log / dead code |
| QUAL-CHAT-03 | 🟢 Medium | Quality | typo ใน path API |
| QUAL-CHAT-04 | 🟢 Medium | Quality | Gateway ลงทะเบียนผิดชั้น module |
| OBS-CHAT-01 | 🟢 Medium | Observability | ไม่มี compose/CI ใน repo |
| OBS-CHAT-02 | 🟢 Medium | Observability | uploads ไม่ยืนยัน persistence |
| OBS-CHAT-03 | 🟢 Medium | Observability | Swagger + ngrok leftover |
Modernization priority (แนะนำลำดับ)
- Rotate + ลบ secrets จาก git (SEC-CHAT-04)
- Parameterize SQL ทุกจุด (SEC-CHAT-01)
- Verify LINE webhook signature (SEC-CHAT-03)
- ใส่ auth บน Socket.IO + แยก internal emit จาก client event (SEC-CHAT-02)
- ปิด
synchronize+ ทำ migration (CORR-CHAT-02) - ย้าย appointment reminder ไป durable scheduler (CORR-CHAT-01)
- 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