Skip to content

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ด้านในด้านนอกหมายเหตุ
1Edge nginx → vtrc-apivtrc-api processทุก HTTP request จาก internetTrust boundary หลัก ทุก input ถือว่า untrusted
2vtrc-api → cu-central-apicu-central-api processvtrc-api เป็น trusted callerGraphQL (END_POINT_CENTRALIZE) + Auth REST (END_POINT_CENTRALIZE_AUTH /signin ฯลฯ) + API_CENTRALIZE_KEY — ควรเป็น mTLS แต่ใช้ static key
3cu-central-api → LDAP/ADLDAPcu-central-apiprimary login credential check = user bind (bindAdWithUserAndPassword) — ไม่ใช่ MSSQL password
4Browser → ทุก backendbackendSPA bundleทุก API call มี Authorization: Bearer + apiKey header
5Container network → hosthostทุก 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 ที่ไม่มี authRCE, data exfiltration, DoS
Authenticated employeeทุก protected field (ตาม role)privilege escalation, อ่านข้อมูล HR ของคนอื่น
Insider with repo accessprod device keys, GCP SA key, .env ที่ commitlateral 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

63:68:vtrc-api/api/src/typeDefs/authentication.js
    forceLoging(
      errorMessage: String
      methode: String
      "json format ex. '{'id': 1, 'text': 'abc'}'"
      param: JSON
    ): ResposeStatus

forceLoging (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เกณฑ์ตัวอย่าง
🔴 Criticalactive risk ต่อ security/data/availability ต้องแก้ทันทีRCE, SQLi, prod secret leak
🟡 Highมี impact จริงแต่ต้องมีเงื่อนไขSSRF, path traversal, weak hashing
🟢 Mediumhygiene/debt ยังไม่อันตรายทันทีmissing security headers, verbose logs

ทุกบทใช้ legend เดียวกัน เพื่อให้นำไป prioritise remediation ในบทที่ 9.11 ได้


อ่านต่อ → 9.2 Authentication flow