2 · Top-20 Critical remediation runbook
Runbook แก้ Critical ระดับแพลตฟอร์ม 20 รายการแรก เรียงตาม ROI จาก Vol 12.5 + ความรุนแรงของ Deep scorecards (benefit-api/06, 2way-api/04, meeting-vtrc-api/04, vtrc-api/10)
English: Each item = evidence → immediate action → verify → residual risk. Do not paste real secret values into tickets.
กลับ Attack surface · ถัดไป Shared-secret blast radius
How to execute
- ทำทีละ item ตามลำดับด้านล่าง เว้นแต่ IR บังคับ rotate secrets พร้อมกัน (item R01 ต้อง coordinate ข้าม repo)
- ทุก merge ต้องมี: ลบ bypass / ปิด
@Public/ parameterized SQL / fail-fast secret — อย่า comment ทิ้งใน prod - หลังแก้: ตรวจ access log ตามที่ scorecard แนะนำ (ถ้ามี)
Legend
| Field | Meaning |
|---|---|
| ID | Canonical deep ID |
| Evidence | file:line (+ branch) |
| Fix | Minimal safe change |
| Done when | Acceptance check |
R01 · Rotate shared JWT SECRET_KEY + committed DB/API secrets (cluster)
| IDs | SEC-BENEFIT-01, SEC-RECRUIT-01, SEC-AIRECRUIT-01, SEC-PMS-02, SEC-CHAT-04, SEC-COMMON-03, SEC-JOBSCHED-01, SEC-MTVTRC-01, SEC-2WAYVTRC-03/03b, SEC-VTRC-RC-01, … (Vol 12.5 secrets cluster) |
| Evidence | e.g. benefit-api/.env:18,25,32,64 (prod); pms-api/.env:41 (uat) shares key with benefit/recruit/ai-recruit; vtrc-common/.env:11,17; meeting-vtrc-api/.envprod |
| Fix | 1) Inventory every repo in Chapter 3 tables 2) Rotate same-time shared JWT 3) Rotate MSSQL/sa/SFTP/SMTP/GCP/OpenAI keys 4) .gitignore + git filter-repo 5) Move to secret manager |
| Done when | Old secrets fail auth; new secrets only in vault/CI; history purged per policy |
| TH | Rotate พร้อมกัน — ถ้า rotate ทีละตัว token ที่ forge จาก secret เก่ายังใช้กับ service ที่ยังไม่หมุน |
R02 · Delete 2way-api backdoor i3admin
| ID | SEC-2WAYAPI-01 |
| Evidence | 2way-api/src/modules/auth/auth.service.ts (branch: prod):184-240 — username === 'i3admin' && password === '…' → jwt.sign 48h level: 1 |
| Fix | Delete entire if block; no feature flag left in prod; audit log for empCode === 'adm9999'; rotate SECRET_JWT_TWOWAY after |
| Done when | Login with that username returns normal SSO failure; grep finds zero i3admin on prod branch |
R03 · Delete 2way-vtrc-api bypass "testnaja" (+ LINE twin)
| IDs | SEC-2WAYVTRC-01, SEC-2WAYVTRC-01b |
| Evidence | middleware/checkbypasstoken.ts:24; middleware/checkByPassTokenLine.ts:30 (branch: prod) |
| Fix | Remove token == "testnaja" both middlewares; review access logs; rotate any tokens used with bypass |
| Done when | Request with that string gets 401; middleware always verifies real JWT |
R04 · Disable cu-central /auth/signinbypass
| ID | SEC-CU-04 |
| Evidence | cu-central-api/main-api/src/routes.js — passwordless signin live (Vol 12.3/12.5) |
| Fix | Remove route from all non-local envs or delete; check historical access logs |
| Done when | Endpoint 404/disabled on UAT+prod; no passwordless path remains |
R05 · Re-enable meeting global auth + fix broken bypass middleware
| IDs | SEC-MTVTRC-05, SEC-MTVTRC-06 |
| Evidence | meeting-vtrc-api/app.ts:104-120 (commented app.all("/meeting/secure/*", …)); middleware/checkbypass.ts:38-78; util/helper/jwtdecode.ts:12-19 — GraphQL query never posted; decodeJwt(token) arity wrong |
| Fix | Uncomment/restore global guard after fixing middleware to mirror checkbypasswhenredirect.ts (verify with SECRET_JWT_VTRC / meeting secret; reject missing Bearer — do not next()) |
| Done when | Unauthenticated POST to secure mutation returns 401; middleware unit test covers missing/invalid token |
R06 · Remove centralize-api blanket @Public()
| IDs | CRIT-03, CRIT-04 |
| Evidence | All controllers @Public() — 17/17 endpoints (Vol 12.3) |
| Fix | Remove @Public(); enable JWT guard; network allowlist service-to-service; audit PII endpoints |
| Done when | Unauthenticated employee GETs return 401; smoke tests with service credentials pass |
R07 · Close benefit medical / batch / upload @Public()
| IDs | SEC-BENEFIT-03, SEC-BENEFIT-04, SEC-BENEFIT-05 |
| Evidence | wdhospitals.controller.ts:24-40; manualBatch.controller.ts:13-34; upload.controller.ts:13-22 (branch: prod) |
| Fix | Remove @Public(); confirm frontend auth headers; add role checks for batch ops |
| Done when | curl without JWT gets 401 on add/update/detail/batch/upload |
R08 · Close pms salary @Public()
| ID | SEC-PMS-01 |
| Evidence | pms-api/src/modules/budget/budget.controller.ts (branch: uat):49-53 — GET /budget/getEmployeeSalary |
| Fix | Remove @Public(); require HR role; review logs for scrape patterns |
| Done when | Unauthenticated salary query 401; authorized HR role still works |
R09 · Close recruitment public blacklist import
| ID | SEC-RECRUIT-03 |
| Evidence | POST /blacklist/import_blacklist @Public() (Vol 12.5 / recruit scorecard) |
| Fix | Remove @Public(); admin-only role |
| Done when | Import without JWT fails |
R10 · Replace vtrc-api execSync qpdf (command injection)
| ID | SEC-VTRC-API-01 |
| Evidence | vtrc-api/api/src/routes.js:73,120,227,277,355 — execSync(`qpdf --encrypt ${passwordPDF} …`) |
| Fix | execFileSync('qpdf', ['--encrypt', pwd1, pwd2, '40', '--', src, dst]); allowlist password charset/length |
| Done when | Metacharacters in password cannot spawn shell; five call sites updated |
R11 · Lock /webdeployment and file download
| IDs | SEC-VTRC-API-03, SEC-VTRC-API-04 |
| Evidence | routes.js:975-984 unauth shell; routes.js:464 download without apiKey |
| Fix | Delete or SUPER_ADMIN + IP allowlist for webdeployment; apiKey+JWT+ownership on download |
| Done when | Anonymous curl returns 401; authorized paths still serve files |
R12 · Fail-fast JWT defaults ('secret' / empty / fallbacks)
| IDs | SEC-VTRC-API-02, SEC-CU-01, SEC-2WAYAPI-02 |
| Evidence | vtrc-api/api/src/config.js:12; cu-central empty default; 2way-api/auth.service.ts:20-26 SSO/AES ` |
| Fix | Remove defaults; throw on boot if required env missing; rotate previously used defaults |
| Done when | Service refuses start without env; no literal fallbacks in source |
R13 · Parameterize benefit SQL (largest surface)
| ID | SEC-BENEFIT-02 |
| Evidence | approvedisaster.service.ts:415-417 empCode = ${query.approverEmpCode}; payments.service.ts:415-573,640-720,1032-1183,1185-1435 |
| Fix | query(sql, [params]); extract shared builder (QUAL-BENEFIT-01) so one patch covers three copies |
| Done when | semgrep/rg finds no ${ inside SQL for these modules; regression tests on list/approve |
R14 · Parameterize 2way-api announces/surveys/complaints SQL
| ID | SEC-2WAYAPI-03 |
| Evidence | announces.service.ts:164-296,297-463; surveys/complaints; dormant PMS cron concat notification-pms.task.service.ts:77-84 |
| Fix | Parameterized entityManager.query; fix cron SQL before re-enabling @Cron |
| Done when | rg "entityManager\\.query\\(\" shows no ${ user params |
R15 · Parameterize recruit EXEC + chat/common SQL
| IDs | SEC-RECRUIT-02, SEC-CHAT-01, SEC-COMMON-02, SEC-VTRC-API-07 |
| Evidence | candidate.repositories.ts:1324-1338 dynamic EXEC; chat.service.ts:103-157; address.repository.ts:82-94; vtrc statLog |
| Fix | Bound parameters / sp_executesql; TypeORM QueryBuilder |
| Done when | Injection PoC patterns from scorecards fail; unit tests cover ids with quotes |
R16 · Close 2way-api @Public() mutations + LINE; restore employees permission
| IDs | SEC-2WAYAPI-04, SEC-2WAYAPI-08, SEC-2WAYLINE-01 |
| Evidence | announces/complaints/line controllers @Public(); employees.controller.ts:33-49 middleware disabled; line-service line.controller.ts:13+ |
| Fix | Inventory @Public() → default-deny; JWT+role on mutations; LINE signature or internal-only; re-enable EmpCodeMiddleware |
| Done when | Unauth complaint assign / LINE multicast 401; employees search requires permission view |
R17 · Secure 2way-vtrc profile + updateIncident
| IDs | SEC-2WAYVTRC-02, SEC-2WAYVTRC-02b, SEC-2WAYVTRC-07 |
| Evidence | getProfile.ts:34-63,79-133 auth commented; getProfile.ts CU twin; updateIncident.ts:23 no middleware |
| Fix | Re-enable LINE middleware; bind lineID from verified profile not body; attach auth on updateIncident |
| Done when | Unauth getProfile/updateIncident fail; cannot fetch other users by guessing lineID |
R18 · workflow-api: verify JWT + stop role-from-sourceDB
| ID | SEC-WORKFLOW-01 |
| Evidence | main.ts:9-82 no global guard; jwt-actor.util.ts:1-4,33-47 decode only; ipd-user.util.ts:20-65 inferHospitalUserRole(sourceDB) |
| Fix | Passport/Nest JWT verify; roles from IdP claims or server ACL — not client sourceDB |
| Done when | Forged base64 payload without signature rejected; role tests cover CHULA/SABC/SOMDEJ |
R19 · Frontend: stop token-in-URL + sanitize XSS hotspots
| IDs | SEC-VTRC-RC-02, SEC-BONEW-01, SEC-VTRC-RC-04, SEC-VTRC-WEB-02, SEC-VTRC-WEB-03 |
| Evidence | vtrc-rc-backoffice App/index.js:24; vtrc-backoffice-new Login/index.js:93-103; dangerouslySetInnerHTML spreads; no CSP |
| Fix | POST/httpOnly cookie SSO handoff; DOMPurify; CSP headers; plan localStorage → httpOnly (SEC-VTRC-RC-05, SEC-VTRC-WEB-01) |
| Done when | No access/refresh in path; XSS fixtures sanitized; CSP report-only then enforce |
R20 · Mobile RSA private key + keystore hygiene
| IDs | SEC-MOBILE-01, SEC-NEW-01, SEC-MOBILE-02 |
| Evidence | vtrc-mobile/src/utils/login.js:16-42; new-vtrc-mobile/src/constants/RSAKey.ts:11-38; android/app/build.gradle:174-179 |
| Fix | Move private ops server-side; rotate key pair; remove keystore passwords from git → CI secrets; plan Play signing impact |
| Done when | Grep finds no PEM private in app source; keystore secrets only in CI |
Supplemental Critical (do after Top-20 or in parallel with R01)
| ID | One-line | Evidence pointer |
|---|---|---|
SEC-MTVTRC-02 | Remove mockuser PII route | mockuser.ts:29-60 |
SEC-2WAYAPI-05 | ignoreExpiration: false | auth.service.ts:31-40 |
SEC-AIRECRUIT-02 | Add role/scope on AI routes | verifyJWT.js:7-40 |
SEC-WORKFLOW-02 | Placeholder .env.example + rotate | .env.example:11,31,44 |
SEC-BENEFIT-06 | Remove /ping-centrail SSRF | app.controller.ts:13-16 |
SEC-JOBSCHED-02 | Auth outbound cron POST | incidents.service.ts:20-21 |
CORR-SSO-01 | Redesign JWT issuance | sso-api/app.service.ts:143-191 |
SEC-REPORTLOG-01 | Purge PII logs from git | request_*.log |
SEC-RCWEB-01 | Rotate AES + stop client encrypt | .env + cryptoJs.js |
Runbook checklist (copy to ticket)
[ ] R01 secrets rotate coordinated (Chapter 3 matrix attached)
[ ] R02 i3admin deleted + JWT_TWOWAY rotated
[ ] R03 testnaja deleted both middlewares
[ ] R04 signinbypass gone
[ ] R05 meeting secure middleware live + verify fixed
[ ] R06 centralize @Public removed
[ ] R07 benefit public medical/batch/upload closed
[ ] R08 pms salary closed
[ ] R09 recruit blacklist closed
[ ] R10 qpdf execFileSync
[ ] R11 webdeployment + download auth
[ ] R12 fail-fast JWT/SSO secrets
[ ] R13 benefit SQL parameterized
[ ] R14 2way SQL parameterized
[ ] R15 recruit/chat/common/vtrc SQL
[ ] R16 2way @Public mutations + LINE + employees
[ ] R17 2way-vtrc profile + updateIncident
[ ] R18 workflow JWT verify
[ ] R19 frontend URL tokens + DOMPurify/CSP
[ ] R20 mobile RSA + keystoreOrdering constraints (do not violate)
| Constraint | Why |
|---|---|
| R01 before declaring “JWT forge closed” | Shared secret — partial rotate leaves forge path |
| R05 middleware fix before or with uncomment | Re-enabling broken guard gives false confidence (SEC-MTVTRC-06) |
R02 then rotate SECRET_JWT_TWOWAY | Backdoor-issued tokens remain valid until expiry/rotate |
| R13 extract shared SQL builder | Three copy-pastes (QUAL-BENEFIT-01) — miss one = injection remains |
| Frontend R19 after backend session design | httpOnly needs API cookie support |