Skip to content

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

  1. ทำทีละ item ตามลำดับด้านล่าง เว้นแต่ IR บังคับ rotate secrets พร้อมกัน (item R01 ต้อง coordinate ข้าม repo)
  2. ทุก merge ต้องมี: ลบ bypass / ปิด @Public / parameterized SQL / fail-fast secret — อย่า comment ทิ้งใน prod
  3. หลังแก้: ตรวจ access log ตามที่ scorecard แนะนำ (ถ้ามี)

Legend

FieldMeaning
IDCanonical deep ID
Evidencefile:line (+ branch)
FixMinimal safe change
Done whenAcceptance check

R01 · Rotate shared JWT SECRET_KEY + committed DB/API secrets (cluster)

IDsSEC-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)
Evidencee.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
Fix1) 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 whenOld secrets fail auth; new secrets only in vault/CI; history purged per policy
THRotate พร้อมกัน — ถ้า rotate ทีละตัว token ที่ forge จาก secret เก่ายังใช้กับ service ที่ยังไม่หมุน

R02 · Delete 2way-api backdoor i3admin

IDSEC-2WAYAPI-01
Evidence2way-api/src/modules/auth/auth.service.ts (branch: prod):184-240username === 'i3admin' && password === '…'jwt.sign 48h level: 1
FixDelete entire if block; no feature flag left in prod; audit log for empCode === 'adm9999'; rotate SECRET_JWT_TWOWAY after
Done whenLogin with that username returns normal SSO failure; grep finds zero i3admin on prod branch

R03 · Delete 2way-vtrc-api bypass "testnaja" (+ LINE twin)

IDsSEC-2WAYVTRC-01, SEC-2WAYVTRC-01b
Evidencemiddleware/checkbypasstoken.ts:24; middleware/checkByPassTokenLine.ts:30 (branch: prod)
FixRemove token == "testnaja" both middlewares; review access logs; rotate any tokens used with bypass
Done whenRequest with that string gets 401; middleware always verifies real JWT

R04 · Disable cu-central /auth/signinbypass

IDSEC-CU-04
Evidencecu-central-api/main-api/src/routes.js — passwordless signin live (Vol 12.3/12.5)
FixRemove route from all non-local envs or delete; check historical access logs
Done whenEndpoint 404/disabled on UAT+prod; no passwordless path remains

R05 · Re-enable meeting global auth + fix broken bypass middleware

IDsSEC-MTVTRC-05, SEC-MTVTRC-06
Evidencemeeting-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
FixUncomment/restore global guard after fixing middleware to mirror checkbypasswhenredirect.ts (verify with SECRET_JWT_VTRC / meeting secret; reject missing Bearer — do not next())
Done whenUnauthenticated POST to secure mutation returns 401; middleware unit test covers missing/invalid token

R06 · Remove centralize-api blanket @Public()

IDsCRIT-03, CRIT-04
EvidenceAll controllers @Public() — 17/17 endpoints (Vol 12.3)
FixRemove @Public(); enable JWT guard; network allowlist service-to-service; audit PII endpoints
Done whenUnauthenticated employee GETs return 401; smoke tests with service credentials pass

R07 · Close benefit medical / batch / upload @Public()

IDsSEC-BENEFIT-03, SEC-BENEFIT-04, SEC-BENEFIT-05
Evidencewdhospitals.controller.ts:24-40; manualBatch.controller.ts:13-34; upload.controller.ts:13-22 (branch: prod)
FixRemove @Public(); confirm frontend auth headers; add role checks for batch ops
Done whencurl without JWT gets 401 on add/update/detail/batch/upload

R08 · Close pms salary @Public()

IDSEC-PMS-01
Evidencepms-api/src/modules/budget/budget.controller.ts (branch: uat):49-53GET /budget/getEmployeeSalary
FixRemove @Public(); require HR role; review logs for scrape patterns
Done whenUnauthenticated salary query 401; authorized HR role still works

R09 · Close recruitment public blacklist import

IDSEC-RECRUIT-03
EvidencePOST /blacklist/import_blacklist @Public() (Vol 12.5 / recruit scorecard)
FixRemove @Public(); admin-only role
Done whenImport without JWT fails

R10 · Replace vtrc-api execSync qpdf (command injection)

IDSEC-VTRC-API-01
Evidencevtrc-api/api/src/routes.js:73,120,227,277,355execSync(`qpdf --encrypt ${passwordPDF} …`)
FixexecFileSync('qpdf', ['--encrypt', pwd1, pwd2, '40', '--', src, dst]); allowlist password charset/length
Done whenMetacharacters in password cannot spawn shell; five call sites updated

R11 · Lock /webdeployment and file download

IDsSEC-VTRC-API-03, SEC-VTRC-API-04
Evidenceroutes.js:975-984 unauth shell; routes.js:464 download without apiKey
FixDelete or SUPER_ADMIN + IP allowlist for webdeployment; apiKey+JWT+ownership on download
Done whenAnonymous curl returns 401; authorized paths still serve files

R12 · Fail-fast JWT defaults ('secret' / empty / fallbacks)

IDsSEC-VTRC-API-02, SEC-CU-01, SEC-2WAYAPI-02
Evidencevtrc-api/api/src/config.js:12; cu-central empty default; 2way-api/auth.service.ts:20-26 SSO/AES `
FixRemove defaults; throw on boot if required env missing; rotate previously used defaults
Done whenService refuses start without env; no literal fallbacks in source

R13 · Parameterize benefit SQL (largest surface)

IDSEC-BENEFIT-02
Evidenceapprovedisaster.service.ts:415-417 empCode = ${query.approverEmpCode}; payments.service.ts:415-573,640-720,1032-1183,1185-1435
Fixquery(sql, [params]); extract shared builder (QUAL-BENEFIT-01) so one patch covers three copies
Done whensemgrep/rg finds no ${ inside SQL for these modules; regression tests on list/approve

R14 · Parameterize 2way-api announces/surveys/complaints SQL

IDSEC-2WAYAPI-03
Evidenceannounces.service.ts:164-296,297-463; surveys/complaints; dormant PMS cron concat notification-pms.task.service.ts:77-84
FixParameterized entityManager.query; fix cron SQL before re-enabling @Cron
Done whenrg "entityManager\\.query\\(\" shows no ${ user params

R15 · Parameterize recruit EXEC + chat/common SQL

IDsSEC-RECRUIT-02, SEC-CHAT-01, SEC-COMMON-02, SEC-VTRC-API-07
Evidencecandidate.repositories.ts:1324-1338 dynamic EXEC; chat.service.ts:103-157; address.repository.ts:82-94; vtrc statLog
FixBound parameters / sp_executesql; TypeORM QueryBuilder
Done whenInjection PoC patterns from scorecards fail; unit tests cover ids with quotes

R16 · Close 2way-api @Public() mutations + LINE; restore employees permission

IDsSEC-2WAYAPI-04, SEC-2WAYAPI-08, SEC-2WAYLINE-01
Evidenceannounces/complaints/line controllers @Public(); employees.controller.ts:33-49 middleware disabled; line-service line.controller.ts:13+
FixInventory @Public() → default-deny; JWT+role on mutations; LINE signature or internal-only; re-enable EmpCodeMiddleware
Done whenUnauth complaint assign / LINE multicast 401; employees search requires permission view

R17 · Secure 2way-vtrc profile + updateIncident

IDsSEC-2WAYVTRC-02, SEC-2WAYVTRC-02b, SEC-2WAYVTRC-07
EvidencegetProfile.ts:34-63,79-133 auth commented; getProfile.ts CU twin; updateIncident.ts:23 no middleware
FixRe-enable LINE middleware; bind lineID from verified profile not body; attach auth on updateIncident
Done whenUnauth getProfile/updateIncident fail; cannot fetch other users by guessing lineID

R18 · workflow-api: verify JWT + stop role-from-sourceDB

IDSEC-WORKFLOW-01
Evidencemain.ts:9-82 no global guard; jwt-actor.util.ts:1-4,33-47 decode only; ipd-user.util.ts:20-65 inferHospitalUserRole(sourceDB)
FixPassport/Nest JWT verify; roles from IdP claims or server ACL — not client sourceDB
Done whenForged base64 payload without signature rejected; role tests cover CHULA/SABC/SOMDEJ

R19 · Frontend: stop token-in-URL + sanitize XSS hotspots

IDsSEC-VTRC-RC-02, SEC-BONEW-01, SEC-VTRC-RC-04, SEC-VTRC-WEB-02, SEC-VTRC-WEB-03
Evidencevtrc-rc-backoffice App/index.js:24; vtrc-backoffice-new Login/index.js:93-103; dangerouslySetInnerHTML spreads; no CSP
FixPOST/httpOnly cookie SSO handoff; DOMPurify; CSP headers; plan localStorage → httpOnly (SEC-VTRC-RC-05, SEC-VTRC-WEB-01)
Done whenNo access/refresh in path; XSS fixtures sanitized; CSP report-only then enforce

R20 · Mobile RSA private key + keystore hygiene

IDsSEC-MOBILE-01, SEC-NEW-01, SEC-MOBILE-02
Evidencevtrc-mobile/src/utils/login.js:16-42; new-vtrc-mobile/src/constants/RSAKey.ts:11-38; android/app/build.gradle:174-179
FixMove private ops server-side; rotate key pair; remove keystore passwords from git → CI secrets; plan Play signing impact
Done whenGrep finds no PEM private in app source; keystore secrets only in CI

Supplemental Critical (do after Top-20 or in parallel with R01)

IDOne-lineEvidence pointer
SEC-MTVTRC-02Remove mockuser PII routemockuser.ts:29-60
SEC-2WAYAPI-05ignoreExpiration: falseauth.service.ts:31-40
SEC-AIRECRUIT-02Add role/scope on AI routesverifyJWT.js:7-40
SEC-WORKFLOW-02Placeholder .env.example + rotate.env.example:11,31,44
SEC-BENEFIT-06Remove /ping-centrail SSRFapp.controller.ts:13-16
SEC-JOBSCHED-02Auth outbound cron POSTincidents.service.ts:20-21
CORR-SSO-01Redesign JWT issuancesso-api/app.service.ts:143-191
SEC-REPORTLOG-01Purge PII logs from gitrequest_*.log
SEC-RCWEB-01Rotate 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 + keystore

Ordering constraints (do not violate)

ConstraintWhy
R01 before declaring “JWT forge closed”Shared secret — partial rotate leaves forge path
R05 middleware fix before or with uncommentRe-enabling broken guard gives false confidence (SEC-MTVTRC-06)
R02 then rotate SECRET_JWT_TWOWAYBackdoor-issued tokens remain valid until expiry/rotate
R13 extract shared SQL builderThree copy-pastes (QUAL-BENEFIT-01) — miss one = injection remains
Frontend R19 after backend session designhttpOnly needs API cookie support

ไป 3 · Shared-secret blast radius