15.1 Critical security debt
บทนี้รวบรวม debt ด้านความปลอดภัยระดับ Critical — ความเสี่ยงที่กำลังเกิดอยู่และต้องจัดการทันที
ทั้ง 7 รายการในบทนี้เป็นเงื่อนไขที่ผู้ไม่ประสงค์ดีสามารถใช้โจมตีระบบได้โดยตรง
รายการทั้งหมด
SEC-1 · Command injection ผ่าน qpdf execSync
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/api/src/routes.js:73,120,210,260,312 |
| ปัญหา | ใช้ execSync(\qpdf --encrypt ${allData.passwordPDF} ...`)) โดย passwordPDFมาจากidentityCardของผู้ใช้ (เลขบัตรประชาชน — ค่าที่ผู้ใช้ควบคุมได้) ถ้าผู้ไม่ประสงค์ดีตั้ง identityCard เป็นสตริงที่มี shell metacharacter เช่น"; rm -rf / #` คำสั่งจะถูก execute ในฐานะ process ของ Node |
| Impact | ผู้ไม่ประสงค์ดีสามารถรันคำสั่งใด ๆ บน server ได้จากการ register บัญชีด้วย identityCard ที่ถูกประดิษฐ์ ผลกระทบ = full server compromise |
| Mitigation ระยะสั้น | เปลี่ยน execSync เป็น execFile (หรือ spawn) ที่รับ argument เป็น array ไม่ผ่าน shell — ทำได้ภายใน 1 วัน |
| แนวทางระยะยาว | ย้าย PDF generation ไป service แยกที่ไม่ใช้ shell เลย (Go + pdfcpu หรือ chromedp) |
ตัวอย่างโค้ดที่เป็นปัญหา:
javascript
let cmd = `qpdf --encrypt ${allData.passwordPDF} ${allData.passwordPDF} 256 -- modify infile.pdf outfile.pdf`;
execSync(cmd); // shell injectionตัวอย่างโค้ดที่ปลอดภัยกว่า:
javascript
execFile('qpdf', [
'--encrypt', passwordPDF, passwordPDF, '256', '--',
'infile.pdf', 'outfile.pdf'
], (err, stdout, stderr) => { /* ... */ });SEC-2 · GraphQL playground + introspection เปิดใน production
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/api/src/index.js:68-69 |
| ปัญหา | playground: true และ introspection: true เปิดโดยไม่ตรวจ IS_PRODUCTION — production endpoint มี UI สำหรับ query และสามารถ introspect schema ทั้งหมดได้ |
| Impact | ผู้ไม่ประสงค์ดีเห็น schema ทั้งหมด รู้ตำแหน่ง mutation ที่ sensitive และทดลอง query ได้ผ่าน UI ที่เปิดสาธารณะ |
| Mitigation ระยะสั้น | ตรวจ IS_PRODUCTION ก่อนเปิด playground — ทำได้ภายใน 30 นาที |
| แนวทางระยะยาว | introspection เป็น opt-in ในแต่ละ environment ผ่าน config ไม่ใช่ default |
SEC-3 · Production device keys เก็บใน source code
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/api/src/config.js:42-148 — 7 production keys + 10 UAT keys เป็น plain UUID |
| ปัญหา | device key (ส่งเป็น apiKey header ทุก request) ถูก commit ใน repo ในรูปแบบ cleartext ผู้ที่เข้าถึง repo สามารถ impersonate เป็นแอปใดก็ได้ |
| Impact | ผู้ไม่ประสงค์ดีที่ได้ read access repo สามารถเรียก API ในฐานะแอปที่ถูกต้อง ข้ามการตรวจสอบประเภทแอป |
| Mitigation ระยะสั้น | ย้าย device key ไป environment variable (เริ่มต้น) หรือ secret manager |
| แนวทางระยะยาว | เก็บ key ในรูปแบบ hash (Argon2id) ใน database เปรียบเทียบแบบ constant time และเลิกใช้ header แบบ static — ใช้ claim ใน signed JWT แทน |
SEC-4 · JWT_SECRET เป็นค่า default 'secret'
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/api/src/config.js:12 |
| ปัญหา | `JWT_SECRET = process.env.JWT_SECRET |
| Impact | authentication bypass ทั้งระบบ — ผู้ไม่ประสงค์ดีสามารถสร้าง JWT ที่เป็น admin ได้โดยไม่ต้อง login |
| Mitigation ระยะสั้น | เปลี่ยนให้ service refuse to start ถ้า JWT_SECRET ไม่ถูกตั้ง — ทำได้ภายใน 30 นาที |
| แนวทางระยะยาว | ใช้ key ที่โหลดจาก KMS หมุนเวียน key เป็นระยะ |
SEC-5 · centralize-api CORS origin '*'
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | centralize-api/src/main.ts:31-35 |
| ปัญหา | CORS origin: '*' + allowedHeaders: '*' บน HR-data service เว็บไซต์ใด ๆ ก็สามารถเรียก central HR API แบบ cross-origin ได้ |
| Impact | ผู้ไม่ประสงค์ดีสร้างเว็บที่หลอกให้ผู้ใช้เข้า แล้วใช้ session ของผู้ใช้เรียก HR API จาก browser ได้ |
| Mitigation ระยะสั้น | กำหนด allow-list ของ origin ที่ชัดเจน (domain ของ frontend ทั้งสองตัว) — ทำได้ภายใน 30 นาที |
| แนวทางระยะยาว | ใช้ CORS middleware ที่มี whitelist ในทุก service |
SEC-11 · GCP service account key commit ใน repo
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/vtrc-gcp-sa-key.json (ไฟล์ปรากฏใน repo — ตาม workspace rule ห้าม log หรือ paste) |
| ปัญหา | GCP service account key ถูก commit ใน repo ผู้ที่เข้าถึง repo สามารถใช้ key นี้เข้าถึงทรัพยากร GCP ทั้งหมดที่ SA มีสิทธิ์ |
| Impact | การเข้าถึง GCP project โดยไม่ได้รับอนุญาต อาจรวมถึงการ push image ปลอม, เข้าถึง bucket, หรือใช้บริการอื่น ๆ |
| Mitigation ระยะสั้น | หมุนเวียน key ทันที ลบไฟล์ออกจาก repo (และ git history ถ้าจำเป็น) |
| แนวทางระยะยาว | ใช้ Workload Identity หรือ short-lived token แทน long-lived service account key |
PERF-7 · phpMyAdmin เปิดสู่สาธารณะบน port 8093
| ข้อมูล | รายละเอียด |
|---|---|
| Evidence | vtrc-api/docker-compose.yml:35-46 |
| ปัญหา | phpMyAdmin container เปิด port 8093 สู่ internet โดย PMA_HOST ชี้ไป database host จริง — เป็น database admin UI ที่เข้าถึงได้จาก internet |
| Impact | ผู้ไม่ประสงค์ดีสามารถ brute-force password หรือ exploit CVE ของ phpMyAdmin เพื่อเข้าถึง database ทั้งหมด |
| Mitigation ระยะสั้น | ลบ phpMyAdmin ออกจาก production compose หรือจำกัดด้วย IP allow-list ที่ firewall — ทำได้ภายใน 1 ชั่วโมง |
| แนวทางระยะยาว | ใช้ bastion host + SSH tunnel สำหรับ database admin ไม่มี database UI เปิดสาธารณะ |
สรุป
| ID | รายการ | Effort ระยะสั้น |
|---|---|---|
| SEC-1 | qpdf command injection | 1 วัน |
| SEC-2 | playground + introspection prod | 30 นาที |
| SEC-3 | device keys ใน repo | 2-3 วัน (ย้าย + rotate) |
| SEC-4 | JWT_SECRET default | 30 นาที |
| SEC-5 | centralize CORS * | 30 นาที |
| SEC-11 | GCP SA key | 1-2 วัน (rotate + cleanup history) |
| PERF-7 | phpMyAdmin เปิดสาธารณะ | 1 ชั่วโมง |
ทั้ง 7 รายการนี้เป็นเป้าหมายของ Wave 0 (stop-the-bleed) ในบท 14.10 — แก้ไขได้ภายใน 1-2 สัปดาห์ โดยไม่ต้องเปลี่ยน architecture