10.1 · Threat model + attack surface
บทนี้ตั้ง trust boundary ของระบบ VTRC — อะไรถือว่า "ข้างใน" อะไรถือว่า "ข้างนอก" และ attacker สามารถส่ง input เข้ามาที่จุดไหนบ้าง ทุกบทถัดไปอ้างถึง boundary ที่วางในบทนี้
Trust boundary diagram
┌──────────────────────────────────────────────────┐
│ INTERNET (untrusted) │
│ employee browser · admin browser · mobile app │
└──────────────────────────────────────────────────┘
│ HTTPS
▼
┌──────────────────────────────────────────────────┐
│ EDGE NGINX (TLS terminate) │
│ config: ./nginx (mount จาก host, ไม่อยู่ใน repo) │
└──────────────────────────────────────────────────┘
│
┌────────────────────────────┼────────────────────────────┐
▼ ▼ ▼
┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
│ vtrc-api (4 replicas) │ │ centralize-api │ │ cu-central-api (×3) │
│ :8081-8085 │ │ NestJS + MSSQL │ │ Node 12 + Apollo │
│ │ │ (ไม่ใช่ live login) │ │ ★ LIVE AUTH PATH │
│ ┌──────────────────┐ │ │ │ │ │
│ │ GraphQL endpoint │──┼──┼──────────────────────┼──┼─▶│ GraphQL /api/graphql │ │
│ │ /api/graphql │ │ │ ┌──────────────────┐ │ │ │ END_POINT_CENTRALIZE │
│ └──────────────────┘ │ │ │ REST + Swagger │ │ │ └──────────────────┘ │
│ ┌──────────────────┐ │ │ │ /api · /docs │ │ │ ┌──────────────────┐ │
│ │ REST routes.js │ │ │ │ JwtAuthGuard │ │ │ │ /auth REST ★ │ │
│ │ (PDF, upload, │ │ │ │ @Public() routes │ │ │ │ /signin → LDAP │◀─┼── signInCD
│ │ webdeployment) │ │ │ └──────────────────┘ │ │ └──────────────────┘ │
│ └──────────────────┘ │ │ │ │ ┌──────────────────┐ │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │ │ │ LDAP/AD bind ★ │ │
│ │ phpMyAdmin :8093 │ │ │ │ MSSQL (HR data) │ │ │ │ (primary auth) │ │
│ │ (PROD-7 🔴) │ │ │ │ dbHRMI_Center │ │ │ └──────────────────┘ │
│ └──────────────────┘ │ │ └──────────────────┘ │ │ ┌──────────────────┐ │
│ ┌──────────────────┐ │ │ │ │ │ MSSQL (HR data) │ │
│ │ Redis :6379 │ │ │ ┌──────────────────┐ │ │ │ dbHRMI_Center │ │
│ │ (no auth) │ │ │ │ .env committed │ │ │ └──────────────────┘ │
│ └──────────────────┘ │ │ │ with prod creds 🔴│ │ │ ┌──────────────────┐ │
│ ┌──────────────────┐ │ │ └──────────────────┘ │ │ │ .env.develop-test│ │
│ │ MariaDB vtrc │ │ │ caller จาก vtrc-api │ │ │ │ committed 🔴 │ │
│ │ MariaDB vtrc- │ │ │ ยังไม่ยืนยัน (E-08) │ │ │ └──────────────────┘ │
│ │ centralize │ │ │ │ │ │
│ └──────────────────┘ │ │ │ │ │
└────────────────────────┘ └────────────────────────┘ └────────────────────────┘5 trust boundary ที่สำคัญ
| # | Boundary | ด้านใน | ด้านนอก | หมายเหตุ |
|---|---|---|---|---|
| 1 | Edge nginx → vtrc-api | vtrc-api process | ทุก HTTP request จาก internet | Trust boundary หลัก ทุก input ถือว่า untrusted |
| 2 | vtrc-api → cu-central-api | cu-central-api process | vtrc-api เป็น trusted caller | GraphQL (END_POINT_CENTRALIZE) + Auth REST (END_POINT_CENTRALIZE_AUTH /signin ฯลฯ) + API_CENTRALIZE_KEY — ควรเป็น mTLS แต่ใช้ static key |
| 3 | cu-central-api → LDAP/AD | LDAP | cu-central-api | primary login credential check = user bind (bindAdWithUserAndPassword) — ไม่ใช่ MSSQL password |
| 4 | Browser → ทุก backend | backend | SPA bundle | ทุก API call มี Authorization: Bearer + apiKey header |
| 5 | Container network → host | host | ทุก container | พังเพราะ network_mode: host ใน nginx (ดูบท 9.8) |
NestJS
centralize-api(port 3000) เป็น trust boundary แยก — ไม่ใช่ live login path (ไม่มี GraphQL/signin) — caller จาก vtrc-api ยังไม่ยืนยัน (E-08) ดู Volume 5.9
Attack surface map
Attack surface = ทุกจุดที่ attacker ส่ง input เข้ามาได้ แบ่งเป็น 4 ประเภท
1. GraphQL mutations + queries (public — ไม่มี @auth)
resolvers/authentication.js
├── login — รับ password plaintext
├── loginBackoffice — รับ password plaintext
├── loginBypass — รับ RSA-signed bypass token
├── logout — รับ sessionId
├── forceLoging 🔴 — รับ errorMessage + param JSON → เขียน log
├── refreshToken 🔴 — รับ token + refreshToken → mint ใหม่โดยไม่ verify signature
├── verifyEmpCodeLogin — รับ empCode + captcha
├── generateCaptcha — รับอะไรก็ไม่รู้
├── domainLists — รับ organization ID
├── loginNote — read-only
└── fetchAuthSessionMoblie — (typo ในชื่อ) รับ sessionทุก mutation/query ข้างต้นเป็นสาธารณะ (public) — ตรวจสอบได้จาก typeDefs/authentication.js ที่ไม่มี directive @auth ทุกบรรทัด ผู้โจมตีที่ถือ apiKey header (ซึ่ง commit อยู่ใน repo — บทที่ 9.5) เรียก endpoint เหล่านี้ได้ทั้งหมด
2. REST routes ใน routes.js (77 KB)
routes.js
├── GET /readfile/prs/:year/:month/:pid/:empcode/... 🔴 execSync qpdf (RCE)
├── GET /tax/:year/:month/:pid/:empcode/... 🔴 execSync qpdf (RCE)
├── GET /tax6month/... 🔴 execSync qpdf (RCE)
├── GET /taxyears/... 🔴 execSync qpdf (RCE)
├── GET /salarycert/:pid/:empcode/... 🔴 execSync qpdf (RCE)
├── app.use /webdeployment 🔴 unauthenticated RCE
├── GET /conicle/getFileFromUrl?url= 🔴 SSRF + arbitrary file write
├── GET /conicle/downloadFile/:id 🟡 path traversal
├── GET /file/:id 🟡 DB-driven path traversal
├── POST /upload 🟡 multer ไม่ validate
├── POST /hospitalslip/upload 🟡 multer ไม่ validate
├── POST /hospitaldocument/upload 🟡 multer ไม่ validate
└── app.use / 🟡 static mount `/storage` ทั้งหมดREST endpoint ส่วนใหญ่ตรวจแค่ apiKey header + timestamp (3600s window) + hash แบบ path segment — ไม่ได้ verify Bearer JWT โดยตรง ทำให้เป็น attack surface ที่กว้างที่สุด
3. GraphQL mutations/queries (protected — มี @auth)
typeDefs/*.js (นอกจาก authentication.js)
├── leave, withdraw, study, fund, profile, ... — ต้องมี @auth(accessRole, accessMenu)
└── แต่ละ field ผ่าน checkSession + checkMenuAuth (บทที่ 9.3 + 9.4)พื้นที่ส่วนนี้มีขนาดใหญ่แต่มี guard — ช่องโหว่หลักคือ RBAC inconsistency (CORR-4, บทที่ 9.4) และ N+1 query performance (PERF-4)
4. Outbound calls (SSRF target)
vtrc-api
├── hrmi.js — rejectUnauthorized: false (TLS ปิด)
├── centralize/controllers/auth.js — axios POST password plaintext → cu-central-api /auth/signin (LDAP)
├── centralize (GraphQL) — END_POINT_CENTRALIZE → cu-central-api GraphQL
├── conicle.js — axios พร้อม session cookie
└── statLog.js — axios ไป END_POINT_LOG_EVENT (input ส่งต่อเข้า SQL)3 actor ที่ต้องกังวล
| Actor | สิ่งที่เข้าถึงได้ | แรงจูงใจที่น่าจะเป็น |
|---|---|---|
| Anonymous internet | ทุก public mutation/query + REST endpoint ที่ไม่มี auth | RCE, data exfiltration, DoS |
| Authenticated employee | ทุก protected field (ตาม role) | privilege escalation, อ่านข้อมูล HR ของคนอื่น |
| Insider with repo access | prod device keys, GCP SA key, .env ที่ commit | lateral movement, ปลอมเอกสาร |
Insider ที่มี repo read สามารถ impersonate แอป VTRC ทุกตัว (เพราะ apiKey commit อยู่ใน config.js:41-148) นี่คือเหตุผลที่บทที่ 9.5 ถือว่า secrets เป็น Critical
ช่องโหว่ที่ "default-deny" ไม่มี
GraphQL schema ของ VTRC ไม่มี default-deny — field ที่ลืมใส่ @auth เป็น public อัตโนมัติ ตัวอย่างที่พบในการ audit
forceLoging(
errorMessage: String
methode: String
"json format ex. '{'id': 1, 'text': 'abc'}'"
param: JSON
): ResposeStatusforceLoging (typo ของ forceLogging) เป็น mutation ที่ public รับ JSON อะไรก็ได้ แล้วเขียนลง log.forceLogs โดยตรง — ผู้โจมตีสามารถ poison audit log หรือทำ log-injection ได้ (รายละเอียดในบทที่ 9.9)
สิ่งนี้สะท้อนว่า VTRC ขาด pattern ที่ schema visitor ตรวจหา field ที่ลืม directive ใน Go target (Volume 15) จะใช้ default-deny + allowlist แทน
Severity ranking ที่ใช้ทั้ง Volume
| Severity | เกณฑ์ | ตัวอย่าง |
|---|---|---|
| 🔴 Critical | active risk ต่อ security/data/availability ต้องแก้ทันที | RCE, SQLi, prod secret leak |
| 🟡 High | มี impact จริงแต่ต้องมีเงื่อนไข | SSRF, path traversal, weak hashing |
| 🟢 Medium | hygiene/debt ยังไม่อันตรายทันที | missing security headers, verbose logs |
ทุกบทใช้ legend เดียวกัน เพื่อให้นำไป prioritise remediation ในบทที่ 9.11 ได้
อ่านต่อ → 9.2 Authentication flow