Skip to content

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

แนวทางแก้:

  1. เปลี่ยน secret ทั้งหมด (rotate) — ทั้ง AES key, MSSQL password, API keys
  2. ลบ .env ออกจาก git history (git filter-repo)
  3. เพิ่ม .env ใน .gitignore
  4. ใช้ 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 ก่อน (แต่ก็เสี่ยง)

แนวทางแก้:

  1. ย้ายการ query Stimulsoft ไปฝั่ง backend — frontend ส่งแค่ "ต้องการ report อะไร"
  2. ลบ REACT_APP_STIMUL_CONN_STRING ออกจาก .env
  3. เปลี่ยน MSSQL password ทันที

C3. SSO bypass /pathLogin/:token/:refreshToken รับ token ใน URL

js
// src/containers/App/index.js
<Route path="/pathLogin/:token/:refreshToken" component={PathLogin} />

ผลกระทบ: token + refreshToken อยู่ใน URL ทำให้รั่วผ่าน:

  1. Browser history — ผู้ใช้ที่กด back เห็น URL ที่มี token
  2. Referer header — ถ้าหน้ามีลิงก์ออกเว็บนอก เว็บปลายทางจะเห็น URL ใน referer
  3. Server log — web server log เก็บ URL ทุก request
  4. Proxy log — proxy ระหว่างทางอาจ log URL
  5. Browser extension — extension ที่อ่าน URL ปัจจุบัน

เพราะ: การส่งผ่านข้อมูล sensitive ใน URL เป็น anti-pattern (URL ไม่ใช่ข้อมูลลับ)

ความเสียหาย: ผู้โจมตีที่ได้ token สามารถ impersonate ผู้ใช้ได้จนกว่า token จะหมดอายุ

แนวทางแก้:

  1. เปลี่ยนเป็น POST body หรือ fragment (#token=...) — fragment ไม่ถูกส่งไป server
  2. ใช้ authorization code flow (OAuth-style) — แลก code เป็น token ฝั่ง server
  3. ถ้าต้องใช้ URL — ใช้ short-lived token (10 วินาที) + แปลงเป็น session token ทันที

High security — 4 จุด

H1. dangerouslySetInnerHTML ใน 30 ไฟล์

jsx
<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:

js
import DOMPurify from "dompurify";
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(news.content) }} />

ข้อระวัง: ทีมต้องตัดสินใจว่าจะอนุญาต HTML บางส่วน (เช่น <b>, <a>) หรือไม่ — DOMPurify รองรับ


H2. cryptoJs.js ใช้ AES key ที่ expose

js
// 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 (เช่น เลขบัตรประชาชน) → ผู้โจมตีถอดได้

แนวทางแก้:

  1. อย่าใช้ AES ฝั่ง client สำหรับข้อมูล sensitive
  2. ถ้าต้องการความลับ — เก็บฝั่ง server เท่านั้น
  3. ถ้าต้องส่งข้อมูลลับระหว่าง client/server — ใช้ HTTPS (transport encryption) ไม่ใช่ AES ฝั่ง client

H3. Token ใน localStorage ทั้งหมด (XSS ขโมยได้)

js
localStorage.setItem("token", data.loginBackoffice.token);
localStorage.setItem("refreshToken", data.loginBackoffice.refreshToken);

ผลกระทบ: XSS ใด ๆ ในแอป (เช่น จาก dangerouslySetInnerHTML) สามารถอ่าน localStorage.getItem("token") และส่งไปยังเซิร์ฟเวอร์ของผู้โจมตี

เพราะ: localStorage เข้าถึงได้จาก JavaScript ใด ๆ ที่รันใน origin เดียวกัน

ความเสียหาย: ผู้โจมตีที่มี XSS จะได้ token และ impersonate ผู้ใช้ได้

ลดความเสี่ยง:

  1. ใช้ HttpOnly cookie แทน localStorage — JavaScript ไม่สามารถอ่านได้
  2. ถ้าต้องใช้ localStorage — ทำ XSS prevention ให้ดี (CSP, sanitize)

H4. apiKey ฝังใน utils/apiKey.js (apiKeyNews)

js
// 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 ที่ถูกต้อง

js
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 แทน


ถ้ามี 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:

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)

แนวทางแก้:

  1. Code splitting ด้วย React.lazy + Suspense สำหรับหน้าใหญ่
  2. ลบ MUI v5 ออก (ถ้าไม่ใช้)
  3. แทน moment ด้วย dayjs (~7 KB vs 300 KB)
  4. Lazy-load Stimulsoft เฉพาะหน้า report

P2. ไม่มี code splitting

js
// ปัจจุบัน — 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

js
console.log = console.warn = console.error = () => {};

ผลกระทบ: debug ไม่ได้ใน production (ต้องใช้ debugger statement)

แนวทางแก้: ใช้ logging library ที่ปิดใน production อัตโนมัติ (เช่น loglevel)


P5. N+1 query

js
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 โตผิดปกติ

แนวทางแก้:

  1. ลบ build/ ออกจาก git
  2. เพิ่ม build/ ใน .gitignore
  3. ใช้ CI/CD สร้าง build แยกต่างหาก

เปรียบเทียบกับ vtrc-web (ดู Volume 8 บท 6.9)

ด้านvtrc-rc-backofficevtrc-web
.env committedใช่ (มี SA password)ไม่ (ดีกว่า)
dangerouslySetInnerHTML30 ไฟล์~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

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

ไป บท 7.10 Scorecard + comparison vtrc-web