8.9 · Security + performance
บทนี้สรุปจุดเสี่ยงด้านความปลอดภัยและปัญหาด้านประสิทธิภาพของ vtrc-rc-backoffice — ทั้ง 2 ด้านมีปัญหาสะสมที่ต้องเร่งจัดการ
ภาพรวม
| ด้าน | ระดับความรุนแรง | จำนวน |
|---|---|---|
| Critical security | สูงมาก | 3 |
| High security | สูง | 4 |
| Moderate security | ปานกลาง | 5 |
| Performance | สูง | 6 |
รายละเอียดเพิ่มเติมและ vector attack เชิงลึกอยู่ใน Volume 10 (Security)
Critical security — 3 จุด
C1. .env ที่มี secret ถูก commit ลง git
# vtrc-rc-backoffice/.env (committed)
REACT_APP_PRIVATE_KEY='aue3uNdEfInEdb565dec6cOtHdn95e8d' ← AES key
REACT_APP_STIMUL_CONN_STRING="Integrated Security=False;Data Source=10.188.128.61;Initial Catalog=:db:;User ID=sa; Password=h@ckm3SA?" ← MSSQL SA password
REACT_APP_AI_RECRUITMENT_API='https://...key=...'
REACT_APP_PMS_API_END_POINT='https://...key=...'ผลกระทบ:
- AES key สำหรับ
cryptoJs.jsรั่ว — ทุกอย่างที่ encrypt ด้วย key นี้ถูกถอดได้ - MSSQL SA password รั่ว — ถ้า MSSQL เปิดให้ internet ผู้โจมตีเข้าถึง DB ได้โดยตรง
- API key ของบริการอื่น ๆ รั่ว — สามารถใช้ยิงบริการนั้น ๆ ได้
เพราะ: .env ถูกบันทึกใน git (สันนิษฐานว่าลืมเพิ่ม .gitignore ตอนเริ่มโปรเจกต์ หรือถูกบังคับ commit)
ความเสียหาย: ทุกคนที่เข้าถึง repo สามารถอ่าน secret ได้ — รวมถึงอดีตพนักงาน, contractor, หรือผู้โจมตีที่ได้ repo access
แนวทางแก้:
- เปลี่ยน secret ทั้งหมด (rotate) — ทั้ง AES key, MSSQL password, API keys
- ลบ
.envออกจาก git history (git filter-repo) - เพิ่ม
.envใน.gitignore - ใช้ secret manager (AWS Secrets Manager, HashiCorp Vault) สำหรับฝั่ง runtime
C2. MSSQL connection string ฝังใน bundle ส่งให้ browser
REACT_APP_STIMUL_CONN_STRING="...User ID=sa; Password=h@ckm3SA?..."ผลกระทบ: CRA ฝัง process.env.REACT_APP_* ใน bundle ที่ส่งไป browser — ทุกคนที่เปิดเว็บสามารถดู connection string ได้จาก DevTools → Sources
เพราะ: Stimulsoft report ต้องใช้ connection string ตรง ๆ เพื่อ query MSSQL ฝั่ง client (design choice ที่ผิดอย่างร้ายแรง)
ความเสียหาย:
- ผู้โจมตีอ่าน password จาก bundle
- ถ้า MSSQL เปิดให้ internet — เข้าถึง DB ได้โดยตรง
- ถ้า MSSQL อยู่ intranet เท่านั้น — ผู้โจมตีต้องเข้า intranet ก่อน (แต่ก็เสี่ยง)
แนวทางแก้:
- ย้ายการ query Stimulsoft ไปฝั่ง backend — frontend ส่งแค่ "ต้องการ report อะไร"
- ลบ
REACT_APP_STIMUL_CONN_STRINGออกจาก.env - เปลี่ยน MSSQL password ทันที
C3. SSO bypass /pathLogin/:token/:refreshToken รับ token ใน URL
// src/containers/App/index.js
<Route path="/pathLogin/:token/:refreshToken" component={PathLogin} />ผลกระทบ: token + refreshToken อยู่ใน URL ทำให้รั่วผ่าน:
- Browser history — ผู้ใช้ที่กด back เห็น URL ที่มี token
- Referer header — ถ้าหน้ามีลิงก์ออกเว็บนอก เว็บปลายทางจะเห็น URL ใน referer
- Server log — web server log เก็บ URL ทุก request
- Proxy log — proxy ระหว่างทางอาจ log URL
- Browser extension — extension ที่อ่าน URL ปัจจุบัน
เพราะ: การส่งผ่านข้อมูล sensitive ใน URL เป็น anti-pattern (URL ไม่ใช่ข้อมูลลับ)
ความเสียหาย: ผู้โจมตีที่ได้ token สามารถ impersonate ผู้ใช้ได้จนกว่า token จะหมดอายุ
แนวทางแก้:
- เปลี่ยนเป็น POST body หรือ fragment (
#token=...) — fragment ไม่ถูกส่งไป server - ใช้ authorization code flow (OAuth-style) — แลก code เป็น token ฝั่ง server
- ถ้าต้องใช้ URL — ใช้ short-lived token (10 วินาที) + แปลงเป็น session token ทันที
High security — 4 จุด
H1. dangerouslySetInnerHTML ใน 30 ไฟล์
<div dangerouslySetInnerHTML={{ __html: news.content }} />ผลกระทบ: ถ้า news.content มี <script> หรือ <img onerror=...>, JavaScript จะรันใน browser ของผู้ใช้
เพราะ: แสดง content จาก backend (ที่กรอกโดย admin) โดยไม่ sanitize
ความเสียหาย:
- ถ้า admin account ถูก compromise → inject script ใน content → ผู้ใช้ทุกคนที่ดู content นั้นโดน XSS
- ถ้า backend ไม่ sanitize ตอนรับ → ผู้ใช้ที่กรอก content (เช่น ใน comment) ก็โจมตีได้
ลดความเสี่ยง: ใช้ DOMPurify ก่อน render:
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(news.content) }} />ข้อระวัง: ทีมต้องตัดสินใจว่าจะอนุญาต HTML บางส่วน (เช่น <b>, <a>) หรือไม่ — DOMPurify รองรับ
H2. cryptoJs.js ใช้ AES key ที่ expose
// src/utils/cryptoJs.js
import CryptoJS from "crypto-js";
const SECRET = process.env.REACT_APP_PRIVATE_KEY; // 'aue3uNdEfInEdb565dec6cOtHdn95e8d'
export const encrypt = (text) => {
return CryptoJS.AES.encrypt(text, SECRET).toString();
};
export const decrypt = (cipher) => {
const bytes = CryptoJS.AES.decrypt(cipher, SECRET);
return bytes.toString(CryptoJS.enc.Utf8);
};ผลกระทบ: ทุกอย่างที่ encrypt ด้วย helper นี้ถูกถอดได้โดยทุกคน (key อยู่ใน bundle)
เพราะ: AES ฝั่ง client ไม่มีความหมายถ้า key expose
ความเสียหาย: ถ้าใช้ encrypt ข้อมูล sensitive (เช่น เลขบัตรประชาชน) → ผู้โจมตีถอดได้
แนวทางแก้:
- อย่าใช้ AES ฝั่ง client สำหรับข้อมูล sensitive
- ถ้าต้องการความลับ — เก็บฝั่ง server เท่านั้น
- ถ้าต้องส่งข้อมูลลับระหว่าง client/server — ใช้ HTTPS (transport encryption) ไม่ใช่ AES ฝั่ง client
H3. Token ใน localStorage ทั้งหมด (XSS ขโมยได้)
localStorage.setItem("token", data.loginBackoffice.token);
localStorage.setItem("refreshToken", data.loginBackoffice.refreshToken);ผลกระทบ: XSS ใด ๆ ในแอป (เช่น จาก dangerouslySetInnerHTML) สามารถอ่าน localStorage.getItem("token") และส่งไปยังเซิร์ฟเวอร์ของผู้โจมตี
เพราะ: localStorage เข้าถึงได้จาก JavaScript ใด ๆ ที่รันใน origin เดียวกัน
ความเสียหาย: ผู้โจมตีที่มี XSS จะได้ token และ impersonate ผู้ใช้ได้
ลดความเสี่ยง:
- ใช้
HttpOnlycookie แทน localStorage — JavaScript ไม่สามารถอ่านได้ - ถ้าต้องใช้ localStorage — ทำ XSS prevention ให้ดี (CSP, sanitize)
H4. apiKey ฝังใน utils/apiKey.js (apiKeyNews)
// src/utils/apiKey.js — ไม่มี getApikey()
export const apiKeyNews = () => {
if (window.location.href.includes("https://vtrcbackoffice.redcross.or.th/")
|| window.location.href.includes("https://rc-vtrcbackoffice.redcross.or.th/")) {
return '…'; // prod UUID
}
// UAT / localhost / Firebase → UAT UUID
};ผลกระทบ: apiKey รั่วใน bundle — ผู้โจมตีสามารถยิง API โดยตรงโดยใช้ apiKey นี้
เพราะ: apiKey ใช้สำหรับระบุแอป (WEB_ADMIN) แต่ฝังตรง ๆ ทำให้ไม่ลับ
ความเสียหาย:
- ผู้โจมตีสามารถเรียก API ผ่าน Postman โดยใช้ apiKey + token ที่ขโมยมา
- ถ้า apiKey เป็นตัวเดียวกันสำหรับผู้ใช้ทุกคน — ไม่เพิ่ม attack surface มาก
- แต่ถ้า apiKey เปลี่ยนตาม environment — ทำให้ debug ยาก
แนวทางแก้: ยอมรับว่า apiKey ไม่ใช่ความลับ (เหมือน client ID) — แต่ต้องเชื่อมั่นใน backend auth (JWT) จริง
Moderate security — 5 จุด
M1. checkLinkURL() ใช้ includes() ไม่ parse URL ที่ถูกต้อง
if (window.location.href.includes("https://vtrcbackoffice.redcross.or.th/")) {
return "https://vtrcapi.redcross.or.th/api/graphql"
}ผลกระทบ: ถ้ามี query string ที่บังเอิญมีคำว่า vtrcbackoffice จะ match ผิด
ลดความเสี่ยง: ใช้ new URL(window.location.href).origin แทน
M2. errorLink refresh token race condition
ถ้ามี request 3 ตัวที่ได้ TOKEN_EXPIRED พร้อมกัน — อาจเรียก refresh 3 ครั้งทำให้ token เก่า invalid ก่อน
ลดความเสี่ยง: ทำ refresh เป็น singleton promise — ถ้ากำลัง refresh อยู่ request อื่นรอ
M3. ไม่มี CSP (Content Security Policy)
ไม่พบ <meta http-equiv="Content-Security-Policy"> หรือ CSP header
ผลกระทบ: XSS ทำงานได้เต็มที่ — โหลด script จาก origin ใด ๆ ก็ได้
ลดความเสี่ยง: เพิ่ม CSP ใน public/index.html:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:;" />M4. ไม่มี CSRF protection
ไม่พบ CSRF token หรือ SameSite cookie
ผลกระทบ: ถ้าใช้ cookie auth — CSRF โจมตีได้ แต่: แอปใช้ Authorization header (ไม่ใช่ cookie) — CSRF จึงไม่เป็นปัญหา
M5. ไม่มี Subresource Integrity (SRI) สำหรับ CDN
ถ้ามีการโหลด script จาก CDN (เช่น Stimulsoft) — ไม่มี SRI
ผลกระทบ: ถ้า CDN ถูก compromise — script ที่โหลดอาจถูกดัดแปลง
Performance — 6 ปัญหา
P1. Bundle ใหญ่ ~11-13 MB
| สาเหตุ | ขนาดประมาณการ |
|---|---|
| AntD v4 full import | ~1.2 MB |
| MUI v5 (ใช้น้อย) | ~300 KB |
| Stimulsoft | ~5 MB |
| moment.js + locale | ~300 KB |
| PDF libraries (3 ตัว) | ~2 MB |
| Excel libraries (2 ตัว) | ~800 KB |
| อื่น ๆ | ~1 MB |
ผลกระทบ: First load ช้า (3-5 วินาทีบน 3G)
แนวทางแก้:
- Code splitting ด้วย
React.lazy+Suspenseสำหรับหน้าใหญ่ - ลบ MUI v5 ออก (ถ้าไม่ใช้)
- แทน
momentด้วยdayjs(~7 KB vs 300 KB) - Lazy-load Stimulsoft เฉพาะหน้า report
P2. ไม่มี code splitting
// ปัจจุบัน — import ทุกอย่างใน index.js (bundle ใหญ่)
import Home from "./pages/Home";
import ManageUser from "./pages/ManageUser";
// ... 200+ pages
// ควรเป็น — lazy load
const Home = React.lazy(() => import("./pages/Home"));
const ManageUser = React.lazy(() => import("./pages/ManageUser"));
// ใช้
<Suspense fallback={<Loading />}>
<Home />
</Suspense>ผลกระทบ: ผู้ใช้ต้อง load ทุกหน้าตอนเปิดแอป แม้จะไม่ได้ใช้
แนวทางแก้: เพิ่ม React.lazy สำหรับ route-level components
P3. fetchPolicy: "no-cache" ทำให้ไม่มี cache
ดูรายละเอียดใน บท 7.3
ผลกระทบ: ทุก navigation ต้อง re-fetch — server โหลดหนัก
แนวทางแก้: ลด scope ของ no-cache — ใช้ cache-first สำหรับข้อมูลไม่ค่อยเปลี่ยน (เช่น role list)
P4. console.log = () => {} ปิด console
console.log = console.warn = console.error = () => {};ผลกระทบ: debug ไม่ได้ใน production (ต้องใช้ debugger statement)
แนวทางแก้: ใช้ logging library ที่ปิดใน production อัตโนมัติ (เช่น loglevel)
P5. N+1 query
data.fetchLeaves.map((leave) => (
<LeaveCard leave={leave} approver={/* fetch แยก */} />
));ดูรายละเอียดใน บท 7.5
ผลกระทบ: ถ้ามี 20 รายการใน list — 20 request แยก
แนวทางแก้: รวม relation ใน query หลัก
P6. 130 MB build/ ที่ commit ลง git
vtrc-rc-backoffice/build/ ← ~130 MB ที่ commit ลง gitผลกระทบ:
- Clone repo ช้า
- Docker image ใหญ่ (เพราะ
COPY . .) - git history โตผิดปกติ
แนวทางแก้:
- ลบ
build/ออกจาก git - เพิ่ม
build/ใน.gitignore - ใช้ CI/CD สร้าง build แยกต่างหาก
เปรียบเทียบกับ vtrc-web (ดู Volume 8 บท 6.9)
| ด้าน | vtrc-rc-backoffice | vtrc-web |
|---|---|---|
.env committed | ใช่ (มี SA password) | ไม่ (ดีกว่า) |
dangerouslySetInnerHTML | 30 ไฟล์ | ~15 ไฟล์ |
| Token ใน localStorage | ใช่ | ใช่ |
| SSO bypass | มี | ไม่มี |
| Stimulsoft + MSSQL string | มี | ไม่มี |
| Bundle | ~11-13 MB | ~12 MB |
| Code splitting | ไม่มี | ไม่มี |
console.* ปิด | ใช่ | ใช่ |
| CSP | ไม่มี | ไม่มี |
| Recoil | มี | ไม่มี |
สรุป: vtrc-rc-backoffice มีความเสี่ยงด้าน security สูงกว่า vtrc-web — โดยเฉพาะการ commit secret ลง git
Checklist สำหรับ security audit
[ ] เปลี่ยน AES key (REACT_APP_PRIVATE_KEY)
[ ] เปลี่ยน MSSQL SA password
[ ] เปลี่ยน API key ทั้งหมด
[ ] ลบ .env ออกจาก git history
[ ] เพิ่ม .env ใน .gitignore
[ ] ย้าย Stimulsoft query ไปฝั่ง backend
[ ] เพิ่ม DOMPurify สำหรับ dangerouslySetInnerHTML
[ ] เพิ่ม CSP ใน public/index.html
[ ] พิจารณาย้าย token จาก localStorage ไป HttpOnly cookie
[ ] เปลี่ยน SSO bypass ให้ไม่ส่ง token ใน URL
[ ] ลบ build/ ออกจาก git