Skip to content

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)

76:91:vtrc-api/api/src/index.js
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

87:87:vtrc-api/api/src/index.js
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

91:91:vtrc-api/api/src/index.js
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: '*'

82:82:vtrc-api/api/src/index.js
    // origin: "*",

แสดงว่า dev รู้ว่า * ผิด แต่ config จริง degrade ไปเป็น * เพราะ typo + default middleware

ผลลัพธ์

  • เว็บไซต์ใด ๆ ในอินเทอร์เน็ตสามารถยิง GraphQL request ข้าม origin ได้
  • ถ้า user login VTRC แล้วเปิดเว็บที่ hostile ใน tab เดียวกัน เว็บนั้นยิง GraphQL mutation ในนาม user ได้ (ปกติควร block โดย CORS)

วิธีแกะ

javascript
// แก้ 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 ไม่ใช่ default

2 · centralize-api CORS origin: '*' (Critical)

31:35:centralize-api/src/main.ts
  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 ของเหยื่อ

วิธีแกะ

typescript
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)

35:46:vtrc-api/docker-compose.yml
  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)

3:13:vtrc-api/docker-compose.yml
  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
158:158:vtrc-api/api/src/config.js
  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 dir exploit (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)

14:34:vtrc-api/docker-compose.yml
  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

🟡 Highnetwork_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

วิธีแกะ

yaml
# ใช้ bridge network + explicit port mapping
networks:
  default:
    driver: bridge

services:
  vtrc-web-proxy:
    # ลบ network_mode: host
    ports:
      - "80:80"
      - "443:443"
    networks:
      - default

6 · MSSQL trustServerCertificate: true (High · SEC-6)

36:38:centralize-api/src/app.module.ts
      extra: {
        trustServerCertificate: true,
      },
55:57:centralize-api/src/app.module.ts
      extra: {
        trustServerCertificate: true,
      },

🟡 HightrustServerCertificate: 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)

5:8:vtrc-api/api/src/lib/repositories/hrmi/hrmi.js
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 แทน

วิธีแกะ

javascript
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)

1:8:vtrc-web/nginx.conf
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: nosniff
  • Referrer-Policy: strict-origin-when-cross-origin
  • Strict-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)

44:47:cu-central-api/docker-compose.yml
    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

วิธีแกะ

yaml
environment:
  - NODE_ENV=production

ตารารางสรุปช่องโหว่ในบทนี้

#ปัญหาSeverityFile:line
1vtrc-api CORS corss typo + default🔴 Criticalindex.js:76-91
2centralize-api CORS *🔴 Criticalmain.ts:31-35
3phpMyAdmin exposed🔴 Criticaldocker-compose.yml:35-46
4Redis no password🟡 Highdocker-compose.yml:3-13 + config.js:158
5network_mode: host🟡 Highdocker-compose.yml:14-34
6MSSQL trustServerCertificate: true🟡 Highapp.module.ts:36-38,55-57
7hrmi.js TLS ปิด🟡 Highhrmi.js:5-8
8Frontend nginx no headers🟢 Mediumvtrc-web/nginx.conf
9NODE_ENV=development ใน prod🟢 Mediumcu-central-api/docker-compose.yml:44,63,80

อ่านต่อ → 9.9 Logging exposure