10.8 · Network + infra exposure
บทนี้ cover ช่องโหว่ที่ infrastructure level — CORS config bug, phpMyAdmin ที่ expose สาธารณะ, Redis ที่ไม่มี password, network_mode: host ที่พัง container isolation, และ TLS verification ที่ปิดไว้
1 · CORS config bug — cosmetic allow-list (Critical)
const corss = {
origin: ['https://vtrc.redcross.or.th',
'https://vtrcbackoffice.redcross.or.th',
'https://uat-vtrc.redcross.or.th',
'https://uat-vtrcbackoffice.redcross.or.th',
'http://localhost:3000'],
// origin: "*",
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'authorization', 'apiKey'],
}
server.applyMiddleware({ app, path: servicePath, bodyParserConfig: { limit: '50mb' }, corss })
app.disable('x-powered-by')
app.use(cors())
app.use(bodyParser.json());🔴 Critical — config allow-list ที่เห็นเป็น "การ์ด" เพราะ typo + default middleware ทำให้มันไม่ทำงานจริง
Bug ที่ 1 — corss typo
server.applyMiddleware({ app, path: servicePath, bodyParserConfig: { limit: '50mb' }, corss })ตัวแปรชื่อ corss (ตัว s เกิน) แต่ Apollo Server's applyMiddleware คาดหวัง property cors — ทำให้ object allow-list ที่ define ไว้ข้างต้น ถูก ignore ทันที เพราะ Apollo ไม่รู้จัก property corss
Bug ที่ 2 — app.use(cors()) ใช้ default
app.use(cors())cors() ไม่มี args จะใช้ default — origin: '*', credentials: false — เป็น wildcard ที่อนุญาตทุก origin
เมื่อ Apollo middleware mount ที่ servicePath (/api/graphql) โดยไม่มี CORS config และ app.use(cors()) ทำงานหลัง server.applyMiddleware — effective policy บน GraphQL endpoint ขึ้นกับ middleware ordering ในทางปฏิบัติ default cors() อนุญาต origin ใด ๆ สำหรับ simple request (non-preflight)
Bug ที่ 3 — comment ทิ้ง origin: '*'
// origin: "*",แสดงว่า dev รู้ว่า * ผิด แต่ config จริง degrade ไปเป็น * เพราะ typo + default middleware
ผลลัพธ์
- เว็บไซต์ใด ๆ ในอินเทอร์เน็ตสามารถยิง GraphQL request ข้าม origin ได้
- ถ้า user login VTRC แล้วเปิดเว็บที่ hostile ใน tab เดียวกัน เว็บนั้นยิง GraphQL mutation ในนาม user ได้ (ปกติควร block โดย CORS)
วิธีแกะ
// แก้ typo + apply ที่ Apollo + อย่าใช้ default cors()
const corsOptions = {
origin: ['https://vtrc.redcross.or.th', /* ... */],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'authorization', 'apiKey'],
credentials: false
}
server.applyMiddleware({ app, path: servicePath, cors: corsOptions })
// ไม่ต้อง app.use(cors()) ถ้า GraphQL ใช้ Apollo's cors
// ถ้าต้องใช้ global ก็ pass options ไม่ใช่ default2 · centralize-api CORS origin: '*' (Critical)
app.enableCors({
origin: '*',
allowedHeaders: '*',
methods: 'GET,PUT,POST,DELETE,PATCH,OPTIONS',
});🔴 Critical (SEC-5) — origin: '*' + allowedHeaders: '*' บน HR data service
เว็บใด ๆ สามารถยิง cross-origin request ไป centralize-api ได้ แม้ JwtAuthGuard จะจำกัดบาง route แต่ @Public() decorator expose unauthenticated routes ซึ่ง reachable จากทุก origin
ถ้า @Public() route คืนข้อมูล sensitive (เช่น dropdown enumeration ที่มีชื่อ employee) → exfiltration จาก browser ของเหยื่อ
วิธีแกะ
app.enableCors({
origin: [
'https://vtrc.redcross.or.th',
'https://vtrcbackoffice.redcross.or.th',
// UAT origins...
],
allowedHeaders: ['Content-Type', 'Authorization', 'apiKey'],
methods: 'GET,POST',
credentials: false
})3 · phpMyAdmin exposed สาธารณะ (Critical · PERF-7)
vtrc-pma:
image: phpmyadmin/phpmyadmin
container_name: vtrc-phpMyAdmin
restart: always
ports:
- "8093:80"
environment:
PMA_ABSOLUTE_URI: http://vtrcapi.redcross.or.th/db-management/
# For localhost
# PMA_ABSOLUTE_URI: http://localhost:8093/
PMA_HOST: vtrc-database
UPLOAD_LIMIT: 100M🔴 Critical — phpMyAdmin bound ที่ port 8093 กับ PMA_HOST: vtrc-database (production MariaDB) และ PMA_ABSOLUTE_URI ชี้ไป vtrcapi.redcross.or.th/db-management/ (URL สาธารณะ)
- ไม่มี auth layer ก่อน phpMyAdmin นอกจาก login ของตัว phpMyAdmin เอง (ซึ่งอ่อน)
UPLOAD_LIMIT: 100Mอนุญาต import ไฟล์ SQL ใหญ่- ใครที่ brute/leak MariaDB credential (และ
.envของ centralize-api ที่ leak ในบท 9.5 มีXfwo_gawPp.!) ได้ GUI เข้า prod DB
วิธีแกะ
- ลบ
vtrc-pmaออกจาก prod compose - DB admin ผ่าน bastion + SSH tunnel เท่านั้น
- หรือถ้าจำเป็น ใช้ IP allowlist ที่ nginx + auth_basic
4 · Redis no password + expose port (High)
vtrc-redis-db:
build:
context: ./docker-redis/
dockerfile: Dockerfile-redis
container_name: vtrc-redis
restart: always
volumes:
- ./redis-db/conf/redis.conf:/usr/local/etc/redis/redis.conf
- ./redis-db/data:/data
ports:
- 6379:6379 REDIS_PASSWORD = '',🟡 High — port 6379 publish ไป host และ REDIS_PASSWORD default เป็น empty string
- ถ้า
redis.confไม่บังคับrequirepass→ Redis เข้าถึงได้ไม่ต้อง auth - Redis เก็บ session data + device-key → APP_TYPE mapping → auth bypass ทั้งหมด
- ที่แย่กว่า — Redis 4.x มี
CONFIG SET direxploit (CVE-2018-1000) ที่เขียน SSH key ผ่าน config
วิธีแกะ
- Bind
127.0.0.1:6379:6379แทน6379:6379 requirepassในredis.conf- ใช้ Redis ACL (6.x+)
5 · network_mode: host พัง isolation (High · PERF-8)
vtrc-web-proxy:
image: nginx:latest
container_name: vtrc-web-server
restart: always
...
ports:
- 80:80
- 443:443
build:
context: ./docker-nginx/
dockerfile: Dockerfile-nginx
network_mode: host🟡 High — network_mode: host ทำให้ nginx container แชร์ network namespace ของ host — เห็นทุก port ที่ bind บน host
- เข้าถึง
:6379(Redis) โดยตรง - เข้าถึง
:3306(MariaDB) ถ้า bind - เข้าถึง
:8093(phpMyAdmin) - เข้าถึงทั้ง 4 vtrc-api instance (
:8081-8085)
defeats container network isolation ทั้งหมด — ถ้า nginx container compromise ได้ (เช่นผ่าน misconfig) attacker เห็นทุก service
วิธีแกะ
# ใช้ bridge network + explicit port mapping
networks:
default:
driver: bridge
services:
vtrc-web-proxy:
# ลบ network_mode: host
ports:
- "80:80"
- "443:443"
networks:
- default6 · MSSQL trustServerCertificate: true (High · SEC-6)
extra: {
trustServerCertificate: true,
}, extra: {
trustServerCertificate: true,
},🟡 High — trustServerCertificate: true ปิด TLS cert verification บน connection ไป MSSQL
- DB traffic ไม่ถูก verify → MITM บน internal network สามารถ intercept/modify query + result
- โดยทั่วไปต้องใช้ real TLS cert จาก internal CA +
trustServerCertificate: false
วิธีแกะ
- Generate cert จาก internal CA
- Mount CA cert เข้า container
trustServerCertificate: false+encrypt: true
7 · hrmi.js rejectUnauthorized: false (High)
import axios from "axios";
import * as log from "../../../lib/wlog";
import https from 'https'
const instance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false,🟡 High — ปิด cert verification สำหรับทุก call ผ่าน instance ไป HRMI service
- MITM บน path ระหว่าง vtrc-api กับ HRMI สามารถ intercept ข้อมูลที่มี PII + JWT
- บางครั้งที DevOps ปิด verification เพราะใช้ self-signed cert — แต่ควร import CA แทน
วิธีแกะ
import fs from 'fs'
const instance = axios.create({
httpsAgent: new https.Agent({
ca: fs.readFileSync('/etc/ssl/certs/internal-ca.pem')
// ไม่ตั้ง rejectUnauthorized — default true อยู่แล้ว
})
})8 · Frontend nginx ไม่มี security headers (Medium)
server {
listen 80;
location / {
root /usr/share/nginx/html/;
include /etc/nginx/mime.types;
try_files $uri $uri/ /index.html;
}
}🟢 Medium — nginx ของ vtrc-web ไม่มี security headers
Header ที่ควรมี
X-Frame-Options: DENY(กัน clickjacking)Content-Security-Policy(กัน XSS)X-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-originStrict-Transport-Security(HSTS)
นอกจากนี้ไม่มี rate limit + ไม่มี request size limit + ไม่มี gzip/brotli (perf issue)
TLS config
Edge TLS terminator อยู่ใน vtrc-api/nginx/ ซึ่ง mount ผ่าน docker-compose.yml:21 — directory นี้ไม่ได้อยู่ใน repo จึง audit cipher suite + HSTS ไม่ได้
9 · NODE_ENV=development ใน prod (Medium)
environment:
- NODE_ENV=development
- TZ=Asia/Bangkok
- APP_NO=1🟢 Medium — container ทั้ง 3 ตัวของ cu-central-api (บรรทัด 44, 63, 80) ship NODE_ENV=development ทั้งที่เป็น production deployment (image gcr.io/vtrc-nonprod/cu-centralize-api:latest)
Apollo Server + Express expose verbose error stack + internals + source map เมื่อ NODE_ENV !== 'production' → information disclosure
วิธีแกะ
environment:
- NODE_ENV=productionตารารางสรุปช่องโหว่ในบทนี้
| # | ปัญหา | Severity | File:line |
|---|---|---|---|
| 1 | vtrc-api CORS corss typo + default | 🔴 Critical | index.js:76-91 |
| 2 | centralize-api CORS * | 🔴 Critical | main.ts:31-35 |
| 3 | phpMyAdmin exposed | 🔴 Critical | docker-compose.yml:35-46 |
| 4 | Redis no password | 🟡 High | docker-compose.yml:3-13 + config.js:158 |
| 5 | network_mode: host | 🟡 High | docker-compose.yml:14-34 |
| 6 | MSSQL trustServerCertificate: true | 🟡 High | app.module.ts:36-38,55-57 |
| 7 | hrmi.js TLS ปิด | 🟡 High | hrmi.js:5-8 |
| 8 | Frontend nginx no headers | 🟢 Medium | vtrc-web/nginx.conf |
| 9 | NODE_ENV=development ใน prod | 🟢 Medium | cu-central-api/docker-compose.yml:44,63,80 |
อ่านต่อ → 9.9 Logging exposure