19.1 · 90-day emergency security fixes
ขอบฟ้า วัน 1–90 — ปิดช่องโหว่ที่ active จาก deep scorecards และ Vol 12.5 backdoor / secrets / SQLi clusters โดยจัดเป็นสัปดาห์ที่ประสานข้าม repo ได้
/ Days 1–90 — close active holes from deep scorecards and the Vol 12.5 clusters, sequenced so shared secrets and backdoors can be closed in coordinated passes.
เป้าหมายของขอบฟ้านี้ไม่ใช่ "rewrite architecture" แต่คือ หยุดเลือด: credential ที่หลุดใช้ไม่ได้, backdoor หาย, injection พื้นผิวหลักปิด, endpoint ที่คืนเงินเดือน/เบิกค่ารักษาโดยไม่ login ถูกบังคับ auth
สัปดาห์ 0 — เตรียม coordination (ก่อน deploy แก้ใด ๆ)
Checklist ก่อนเริ่ม
| ขั้น | งาน | ทำไม |
|---|---|---|
| 0.1 | นัดเจ้าของ repo ของ secrets cluster ทั้งชุด | shared SECRET_KEY / sa ต้อง rotate พร้อมกัน |
| 0.2 | สำรอง access log / deploy manifest ของ UAT+prod | ใช้ตรวจ traffic ผิดปกติหลังปิด @Public() |
| 0.3 | เปิด channel ร่วม + freeze feature ที่แตะ auth | ลด merge conflict ตอนลบ backdoor |
| 0.4 | อ่าน 19.4 What NOT to rewrite | ห้าม "แก้ typo ไปด้วย" ใน PR security |
Secrets ที่ต้อง rotate พร้อมกัน (ชุด A — Domain JWT)
จาก Vol 12.5 secrets cluster และ pms-api SEC-PMS-02:
| Repo | Finding | สิ่งที่หลุด |
|---|---|---|
benefit-api | SEC-BENEFIT-01 | MSSQL ×3 + SECRET_KEY + SFTP password hardcode — benefit-api/06 |
recruitment-api | SEC-RECRUIT-01 | MSSQL + JWT + SMTP + reCAPTCHA + GCP SA |
ai-recruitment-api | SEC-AIRECRUIT-01 | OpenAI/remove.bg + DB |
pms-api | SEC-PMS-02 | UAT .env + shared SECRET_KEY + GCP SA — pms-api/06 |
กฎ: ค่า SECRET_KEY เดียวกัน → rotate ทุก service ในชุด A ในนาทีเดียวกัน แล้ว redeploy พร้อมกัน — มิฉะนั้น JWT ที่ forge จาก secret เก่ายัง valid กับ service ที่ยังไม่ rotate
Secrets ชุด B — MSSQL sa / shared ops
| Repo | Finding | หมายเหตุ |
|---|---|---|
vtrc-common | SEC-COMMON-03 | .env + Docker bake |
job-scheduler-api | SEC-JOBSCHED-01 | .env with sa password — share กับชุด B |
2way-vtrc-api | SEC-2WAYVTRC-03 | Mongo + MSSQL sa in source |
chat-center-api | SEC-CHAT-04 | prod/UAT .env |
meeting-vtrc-api | SEC-MTVTRC-01 | .env* plaintext |
vtrc-rc-backoffice | SEC-VTRC-RC-01 | Stimulsoft conn + AES key — vtrc-rc-backoffice/07 ROI #1 |
สัปดาห์ 1 — Day-one kills (effort ชั่วโมง, impact Critical)
งานเหล่านี้ตรงกับ ROI อันดับต้นของหลาย scorecard และ quick-win เดิม — ทำวันแรกได้โดยไม่รอ secret manager
1.1 ลบ backdoor login / bypass token
| ID | Repo | Evidence (deep) | Remediation ขั้นต่ำ |
|---|---|---|---|
SEC-2WAYAPI-01 | 2way-api | auth.service.ts (branch: prod):184-240 — hardcode i3admin → JWT admin 48h | ลบทั้ง if block; อย่า comment ไว้ — 2way-api/04 |
SEC-2WAYVTRC-01 | 2way-vtrc-api | bypass token "testnaja" ใน middleware | ลบ string check |
SEC-CU-04 | cu-central-api | /auth/signinbypass passwordless | ลบ endpoint |
SEC-MTVTRC-05 | meeting-vtrc-api | global /meeting/secure/* auth commented | uncomment / restore guard |
หลังลบ SEC-2WAYAPI-01: rotate SECRET_JWT_TWOWAY ทันที — token ที่ออกจาก backdoor ยังใช้ได้จนหมดอายุ 48h (2way-api/04 Remediation)
ตัวอย่าง verification:
# ต้องล้มหลัง patch — อย่าใส่รหัสจริงใน ticket; ใช้ค่าจาก scorecard เฉพาะใน controlled test
curl -X POST "https://<host>/2way-api/api/v1/auth/" \
-H "Content-Type: application/json" \
-d '{"username":"i3admin","password":"<known-backdoor>"}'
# expect: ไม่ได้ JWT admin / ไม่ได้ mockProfile.level=11.2 ถอด @Public() ที่คืน PII / เงินเดือน / write ทางการเงิน
| ID | Repo | Endpoint / surface | ROI source |
|---|---|---|---|
SEC-PMS-01 | pms-api | GET /budget/getEmployeeSalary — budget.controller.ts (branch: uat):49-53 | ROI #1 — pms-api/06 |
SEC-BENEFIT-03 | benefit-api | @Public() wdHospitals CRUD — wdhospitals.controller.ts:24-40 | ROI #4 — benefit-api/06 |
SEC-BENEFIT-04 / -05 | benefit-api | batchJob / upload @Public() | ปิดคู่กับ -03 |
SEC-RECRUIT-03 | recruitment-api | @Public() import_blacklist | Vol 12.5 backdoor cluster |
SEC-2WAYLINE-01 | 2way-line-service | ทุก endpoint @Public() | ถอด decorator ในไฟล์เดียว |
CRIT-03 | centralize-api | @Public() ทุก route | auth machinery dead — Vol 12.5 |
ข้อควรระวัง: dropdown ของ benefit ที่เป็น @Public() อาจถูก frontend เรียกก่อน login — อย่าถอดโดยไม่ verify (ดู benefit-api Public contract และ 19.4)
Regression ขั้นต่ำหลัง SEC-PMS-01 (pms-api/06 checklist):
GET /budget/getEmployeeSalaryโดยไม่มี token →401- Login ปกติด้วย JWT + apiKey ยังผ่าน
checkRoleRcได้
1.3 Fail-fast JWT / secret defaults
| ID | Repo | การแก้ |
|---|---|---|
SEC-VTRC-API-02 | vtrc-api | ลบ default JWT_SECRET = 'secret' — throw ตอน boot — vtrc-api/10 ROI #3 |
SEC-CU-01 | cu-central-api | JWT_SECRET default empty → fail-fast |
SEC-2WAYAPI-02 | 2way-api | ลบ AES/SSO fallback ในซอร์ส — 2way-api/04 ROI #2 |
Pattern ที่แนะนำจาก 2way-api/04 remediation sample:
const secret = process.env.SSO_AES_SECRET_KEY;
if (!secret) {
throw new Error('SSO_AES_SECRET_KEY is required');
}1.4 Frontend day-one (XSS / secret hygiene)
| ID | Repo | งาน | ROI |
|---|---|---|---|
SEC-VTRC-WEB-02 | vtrc-web | wrap dangerouslySetInnerHTML ด้วย dompurify | ROI #1 — vtrc-web/07 |
SEC-VTRC-WEB-03 | vtrc-web | CSP header | ROI #2 |
SEC-VTRC-RC-01 | vtrc-rc-backoffice | ลบ .env จาก tracking + rotate | ROI #1 — vtrc-rc-backoffice/07 |
SEC-VTRC-RC-04 | vtrc-rc-backoffice | dompurify | ROI #3 |
SEC-VTRC-RC-06 | vtrc-rc-backoffice | CSP header | ROI #4 |
LEG-CU-01 | cu-central-api | แก้ typosquat axois → axios | supply-chain, effort S |
สัปดาห์ 2–3 — Rotate secrets + scrub git history
ลำดับการทำ (อย่าสลับ)
- Deploy โค้ดที่อ่าน secret จาก env ใหม่ (ยังไม่ rotate) — ยืนยัน boot ไม่ fallback
- Rotate ค่าจริงใน secret store / env ของ cluster ชุด A พร้อมกัน → redeploy ชุด A
- Rotate ชุด B (
sa/ Stimulsoft / Mongo) พร้อมกัน - Invalidate session — logout บังคับหรือรอ TTL สั้นสุดที่มี
git filter-repo/ BFG ลบ.env,*-sa-key.jsonจาก history ทุก repo ใน cluster- Force-push policy + แจ้งทุก clone ให้ re-clone
- ติดตั้ง secret scanning (gitleaks / GitHub secret scanning) เป็น required check — กันรั่วซ้ำ
อ้าง remediation ของ benefit-api SEC-BENEFIT-01 และ pms-api SEC-PMS-02
สิ่งที่ต้อง rotate นอก JWT
| ประเภท | ตัวอย่างที่พบ | Finding |
|---|---|---|
| MSSQL passwords | benefit ×3 schemas, pms ×4, recruitment | secrets cluster |
| SFTP / FMIS | hardcode ใน exportpaymentreport.service.ts:5271,5824,6151 | SEC-BENEFIT-01 |
| SMTP | pms / recruitment | SEC-PMS-02, SEC-RECRUIT-01 |
| GCP SA JSON | pms-api/vtrc-gcp-sa-key.json, recruitment | อย่า paste ใน chat/ticket |
| Device API keys | hardcode ใน configuration.ts (pms) | ย้าย env + rotate |
| AES / Stimulsoft | rc-backoffice .env | SEC-VTRC-RC-01 |
Effort โดยประมาณจาก pms-api ROI #3: 1–3 วัน (ประสานหลายทีม) — ไม่ใช่ "แก้ไฟล์เดียว"
สัปดาห์ 3–5 — SQL injection cluster
จาก Vol 12.5 SQLi cluster และ ROI ของ benefit / 2way:
| ID | Repo | Evidence สั้น | Effort |
|---|---|---|---|
SEC-BENEFIT-02 | benefit-api | payments.service.ts ~700+ บรรทัด dynamic SQL; approvedisaster.service.ts:415-417 — empCode = ${query.approverEmpCode} | L — ROI #2–3 |
SEC-2WAYAPI-03 | 2way-api | announces / surveys / complaints string concat | M — ROI #4 |
SEC-RECRUIT-02 | recruitment-api | dynamic EXEC AI compare | M |
SEC-CHAT-01 | chat-center-api | rooms/tags query string | M |
SEC-VTRC-API-07 | vtrc-api | template literal raw SQL | S–M |
SEC-COMMON-02 | vtrc-common | address provCode/amphCode | S |
ลำดับ execution ที่แนะนำ: แก้จุดเล็ก (SEC-COMMON-02, SEC-VTRC-API-07) ก่อนเพื่อได้ momentum แล้วแยก sprint สำหรับ SEC-BENEFIT-02 พร้อม extract shared WDHospitals SQL builder (benefit-api ROI #3)
Parameterized pattern จาก 2way-api/04:
const rows = await this.entityManager.query(
`SELECT id, topic_name FROM t_content
WHERE emp_code = @0 AND offer_date >= @1 AND offer_date <= @2`,
[empCode, fromDate, toDate],
);และจาก benefit-api SEC-BENEFIT-02:
await this.vtrcEntityManager.query(
'SELECT * FROM MasterApprovers WHERE empCode = @0 AND isActive = 1',
[empCode],
);สัปดาห์ 4–6 — Command injection + unauthenticated routes (vtrc-api)
ตรง vtrc-api/10 ROI #1 และ #4:
| ID | ที่ | การแก้ |
|---|---|---|
SEC-VTRC-API-01 | routes.js:73,120,227,277,355 — execSync(qpdf --encrypt ${passwordPDF}...) | เปลี่ยนเป็น execFileSync('qpdf', [...args]) หรือ worker; validate password alphanumeric |
SEC-VTRC-API-03 | /webdeployment shell binary | เพิ่ม @auth / secret header หรือปิดใน prod |
SEC-VTRC-API-04 | /file/:id/download ไม่มี apiKey | copy middleware จาก route ข้างเคียง |
SEC-VTRC-API-06 | CORS typo corss | แก้ key name พร้อม CORS allowlist fix — ดู 19.4 เพราะ corss เป็น public-ish config key |
SEC-VTRC-API-01 เป็น ROI #1 ของ vtrc-api เพราะปิดทั้ง security และ performance (event-loop block จาก execSync) ในครั้งเดียว
สัปดาห์ 5–8 — Auth design ที่พัง (ไม่ใช่แค่ bypass)
จาก Vol 12.5 broken auth design:
| ID | Repo | ปัญหา | งานใน 90 วัน |
|---|---|---|---|
CORR-SSO-01 | sso-api | JWT sign() commented + JwtService not injected | redesign หรือลบ guard ที่ทำให้ 2FA พัง — เริ่ม design ในสัปดาห์ 5; ship ขั้นต่ำในสัปดาห์ 8 |
SEC-2WAYAPI-05 | 2way-api | ignoreExpiration: true | ตั้ง false — ROI #5 |
SEC-WORKFLOW-01 | workflow-api | ไม่ verify JWT; role จาก sourceDB | เริ่ม spike — อาจล้นไป 6-month ถ้า L |
SEC-AIRECRUIT-02 | ai-recruitment-api | signature only — no role | เพิ่ม role check |
SEC-JOBSCHED-02 | job-scheduler-api | cron POST ไม่มี auth | ใส่ service secret / internal-only |
SEC-2WAYAPI-04 | 2way-api | @Public() บน complaints / LINE / announce mutations | คุย frontend/LINE ก่อนตัด — ROI #3 |
สัปดาห์ 6–10 — Token-in-URL mitigation (ขั้นฉุกเฉินก่อน rewrite เต็ม)
Deep findings: SEC-VTRC-RC-02 (/pathLogin/:token/:refreshToken), SEC-BONEW-01 — Vol 12.5 frontend cluster
HttpOnly cookie เต็มรูปแบบเป็น investment ใหญ่ (vtrc-web ROI #6, vtrc-rc-backoffice ROI #7) — อย่าพยายามทำครบใน 90 วัน
งานฉุกเฉินที่ทำได้ก่อน:
- หยุด access-log ที่ log path/query ที่มี token (nginx / gateway)
- ย้าย SSO handoff จาก path segment ไป POST body (vtrc-rc-backoffice ROI #2)
- ลด TTL ของ token ที่ยังต้องอยู่ใน URL ชั่วคราว
- Audit referrer / analytics ที่อาจเก็บ URL
งานเต็ม (HttpOnly + CSRF) ไปอยู่ 19.3
สัปดาห์ 8–12 — Mobile RSA + infra hardening (เริ่มได้ / อาจล้น)
| ID | Repo | หมายเหตุ |
|---|---|---|
SEC-MOBILE-01 / SEC-NEW-01 | vtrc-mobile / new-vtrc-mobile | RSA private key hardcode — ต้องคู่กับ app store release |
SEC-CU-06 | cu-central-api | TLS / encrypt hardening |
SEC-2WAYLINE-02 | 2way-line-service | hardcoded IP ไม่มี TLS จาก 2way-api |
SEC-2WAYAPI-06 / -07 | 2way-api | DB encrypt + LINE URL จาก env + HTTPS — ROI #7–8 |
ถ้า app-store cycle ไม่ทัน 90 วัน — บันทึกเป็น spillover ไปเดือน 4 พร้อม interim network ACL
Definition of Done — 90 วัน
ถือว่าขอบฟ้า 90 วันสำเร็จเมื่อ:
| # | เกณฑ์ | หลักฐาน |
|---|---|---|
| 1 | Backdoor cluster หายจาก canonical branch ที่ deploy | PR merged + curl regression ล้ม |
| 2 | Secrets ชุด A+B ถูก rotate และไม่เหลือใน working tree | secret scan clean + history scrubbed |
| 3 | @Public() บน salary / wdHospitals write / blacklist import ปิด | 401 โดยไม่มี token |
| 4 | SQLi จุด Critical ใน cluster มี parameterized หรือถูก feature-flag ปิด | code review + smoke test |
| 5 | execSync qpdf ไม่รับ shell string อีก | execFileSync หรือ equivalent |
| 6 | ไม่มี PR ที่ "แก้ typo public contract" ปนมา | checklist 19.4 |
สิ่งที่ตั้งใจเลื่อนออกนอก 90 วัน
- NestJS consolidation / แยก REST PDF worker — 19.3
- Central secret manager เต็มรูปแบบ (หลัง rotate มือเสร็จ)
- Sequelize 5→6, Node 12→20, Apollo v2→v4 — vtrc-api/10 ระบุว่าไม่คุ้มถ้าไม่มี feature ใหม่บังคับ
- รวม
vtrc-backoffice+vtrc-backoffice-new - แก้
CORR-VTRC-API-01transaction arg ทั้งระบบ — ไป 19.2