Skip to content

7.9 · Security + performance

บทนี้สรุปจุดเสี่ยงด้านความปลอดภัยและปัญหาประสิทธิภาพของ vtrc-web โดยอ้างอิงถึง file:line ที่ต้องระวัง

ถ้าจะทำ security review — บทนี้คือจุดเริ่มต้น และควร cross-check กับ Volume 9 (Security)


Security

S-1 · Token เก็บใน localStorage/sessionStorage (ความเสี่ยงสูง)

utils/auth.js:4-35 เก็บ JWT + refreshToken ใน localStorage หรือ sessionStorage

9:20:vtrc-web/src/utils/auth.js
	if (isRemember) {
		localStorage.setItem('token', accessToken)
		if(refreshToken){
			localStorage.setItem('refreshToken', refreshToken)
		}
		localStorage.setItem('isRemember', 'true')
	} else {
		sessionStorage.setItem('token', accessToken)
		if(refreshToken){
			sessionStorage.setItem('refreshToken', refreshToken)
		}
	}

ความเสี่ยง: ทุก JavaScript ที่รันในหน้าเว็บสามารถอ่าน token ได้ผ่าน localStorage.getItem('token') — ถ้ามี XSS ผู้โจมตีขโมย token ได้ทันที

การลดความเสี่ยง: ทางที่ดีควรเก็บ token ใน HttpOnly cookie แต่การเปลี่ยนแปลงนี้กระทบทั้งระบบ (frontend + backend + token flow)

S-2 · apiKey เข้ารหัส base64 ใน bundle

utils/apiKey.js:1-12:

1:12:vtrc-web/src/utils/apiKey.js
export const apiKeyNews = () => {
    if(window.location.href.includes("https://vtrc.redcross.or.th/")){
        // return 'fd77b56f-1489-4fb1-a094-a614f0d16198'
        return 'ZmQ3N2I1NmYtMTQ4OS00ZmIxLWEwOTQtYTYxNGYwZDE2MTk4'
    }else if(window.location.href.includes("https://uat-vtrc.redcross.or.th/")){
        // return '1080670e-926f-49e3-b798-63bb75f47b7d'
        return 'MTA4MDY3MGUtOTI2Zi00OWUzLWI3OTgtNjNiYjc1ZjQ3Yjdk'
	}else if(window.location.href.includes("http://localhost:3000/") || window.location.href.includes("http://192.168.0.105:3000/")){
        return 'MTA4MDY3MGUtOTI2Zi00OWUzLWI3OTgtNjNiYjc1ZjQ3Yjdk'
        // return '1080670e-926f-49e3-b798-63bb75f47b7d'
    }
}

ความเสี่ยง: apiKey เป็น base64 ของ UUID — decode ด้วย atob() ได้ค่าจริงทันที ค่า prod จริงคือ fd77b56f-1489-4fb1-a094-a614f0d16198 ฝังอยู่ใน bundle ทุกคนที่เปิดเว็บจะเห็นได้

base64 ไม่ใช่การเข้ารหัส — เป็นเพียง encoding เพื่อไม่ให้ grep หา UUID pattern เจอง่าย

ผลกระทบ: apiKey ใช้ระบุ APP_TYPE: WEB ฝั่ง server — ถ้าผู้โจมตีได้ apiKey มาสามารถยิง GraphQL ในนาม "WEB" ได้โดยไม่ต้อง login (สำหรับ public query เช่น login, generateCaptcha)

การลดความเสี่ยง: ฝั่ง server ต้องมี rate limit + WAF — apiKey เป็นเพียงการระบุประเภท client ไม่ใช่ authentication จริง

S-3 · AES key สำหรับ "remember password" hardcoded

utils/auth.js:37-47:

37:47:vtrc-web/src/utils/auth.js
export const passwordEncrypt = function(data) {
   let encryptLocalStorage = CryptoJS.AES.encrypt(JSON.stringify(data), "6A45soD%9eB#",
   {
      keySize: 128 / 8,
      iv: "6A45soD%9eB#",
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7
   }).toString();

   localStorage.setItem('profileDetailPW', encryptLocalStorage)
}

ความเสี่ยง: AES key "6A45soD%9eB#" และ IV เดียวกัน hardcoded ใน source — ทุกคนที่เปิด bundle จะเห็น key สามารถ decrypt พาสเวิร์ดที่เก็บใน localStorage ได้ทันที

IV ที่เหมือนกับ key เป็นความผิดพลาดร้ายแรงในการเข้ารหัส — IV ต้องสุ่มทุกครั้ง และต้องไม่ตรงกับ key

ผลกระทบ: ถ้าผู้โจมตีได้ localStorage ของเครื่องผู้ใช้ (เช่นผ่าน malware หรือ physical access) สามารถถอดรหัส password ได้ทันที

การลดความเสี่ยง: ห้ามใช้ "remember password" ในรูปแบบนี้ — ทางที่ดีให้ใช้ refresh token + autologin แทน ไม่ต้องเก็บ plaintext-equivalent ของ password

S-4 · dangerouslySetInnerHTML ใช้ในหลายหน้า

พบใน 20+ ไฟล์ ส่วนใหญ่ render HTML ที่ได้จาก server (news content, payroll slip text, notification)

ความเสี่ยง: ถ้า content จาก server มี <script> หรือ attribute onerror โดยไม่ผ่าน sanitizer ผู้ใช้จะถูก XSS

การลดความเสี่ยง: ฝั่ง server ต้อง sanitize HTML ก่อนเก็บ หรือฝั่ง frontend ต้องใช้ DOMPurify.sanitize(html) ก่อน dangerouslySetInnerHTML

ปัจจุบันไม่พบ sanitizer ใน repo — เป็นช่องโหว่ที่ควรเพิ่ม

S-5 · isShow เป็นเพียง flag ฝั่ง UI

ดู บท 6.4 — route ทั้งหมดถูกสร้างเสมอ ผู้ใช้ที่พิมพ์ URL ตรง ๆ จะเข้าได้

ผลกระทบ: ผู้ใช้ที่ไม่มีสิทธิ์ PMS สามารถเข้า /pms/indicators/list ได้ — หน้าจะ render แต่ทุก query จะ fail ด้วย FORBIDDEN

การลดความเสี่ยง: ฝั่ง server คุมผ่าน @auth directive อยู่แล้ว — แต่ UX ไม่ดี ถ้าต้องการป้องกันที่ฝั่ง frontend ต้องเพิ่ม <PrivateRoute> ที่เช็ค role

S-6 · Console override ทำให้ debug ฝั่ง prod ได้ยาก

App/index.js:15:

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

ผลกระทบ: ถ้าเกิด error ใน production จะ debug ได้ยากเพราะ console ทั้งหมดถูกปิด

การลดความเสี่ยง: เปลี่ยนเป็น strip ใน webpack build แทน (ด้วย terser-webpack-plugin + drop_console) แทนการ override ตอน runtime

S-7 · Google Analytics ID เปิดเผย

public/index.html:33-39:

33:38:vtrc-web/public/index.html
    <script async src="https://www.googletagmanager.com/gtag/js?id=G-LKXD2EBDZ4"></script>
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){dataLayer.push(arguments);}
      gtag('js', new Date());
      gtag('config', 'G-LKXD2EBDZ4');
    </script>

GA4 ID G-LKXD2EBDZ4 เปิดเผยอยู่ใน HTML — เป็น public ID อยู่แล้วตามธรรมชาติ แต่ทำให้ผู้อื่นยิง event เทียมเข้า GA ของระบบได้ (spam หรือ skew analytics)

S-8 · ไม่มี Content Security Policy (CSP)

ไม่พบ CSP header ใน nginx-config/default.conf — ป้องกัน XSS และ data exfiltration ได้ยาก

การลดความเสี่ยง: เพิ่ม CSP header ใน nginx config:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://www.googletagmanager.com; ...";

Performance

P-1 · Bundle ใหญ่ ~12 MB (รวม chunks)

จากการตรวจสอบ build/static/js/:

ไฟล์ขนาดgzip โดยประมาณ
2.72598b5c.chunk.js4.0 MB~1.3 MB
2.bbb73d14.chunk.js3.7 MB~1.2 MB
main.e840df76.chunk.js2.4 MB~800 KB
main.6aa492c2.chunk.js1.9 MB~600 KB
รวม~12 MB~3-4 MB

รวม build/ ทั้งโฟลเดอร์ 79 MB (รวม CSS, fonts, PDF, images)

สาเหตุ:

  1. ไม่มี code splitting ระดับ route — ทุก page อยู่ใน main bundle
  2. moment.js รวม locale ทั้งหมด (มี IgnorePlugin แล้ว แต่ยังใหญ่)
  3. AntD v3 import ทั้ง library (ไม่มี tree-shake)
  4. ใช้ PDF libraries หลายตัว (@react-pdf/renderer, react-pdf, @react-pdf-viewer, pdfjs-dist) — ใหญ่มาก
  5. custom-antd.css 21,190 บรรทัด compile แล้ว ไม่ได้ minify

ผลกระทบ: เวลาโหลดหน้าแรกช้า โดยเฉพาะบนมือถือหรือเน็ตช้า

การลดความเสี่ยง (ถ้าจะแก้):

  1. ใช้ React.lazy + Suspense สำหรับ route-level splitting — แต่ต้องเปลี่ยน pattern จาก class component เป็น lazy ที่ซับซ้อน
  2. ใช้ dayjs แทน moment (เล็กกว่ามาก) — แต่ต้องแก้ helper ทั้งหมดใน utils/formatdate.js
  3. ใช้ babel-plugin-import สำหรับ AntD ทำ per-component import — ลดขนาดลงได้มาก
  4. เลือก PDF library ตัวเดียว — ตัด 3 ตัวที่เหลือออก

P-2 · fetchPolicy: 'no-cache' ทำให้ทุก query ดึงใหม่

apollo.js:171-182:

171:182:vtrc-web/src/config/apollo.js
apolloClient.defaultOptions = {
	watchQuery: {
		fetchPolicy: 'no-cache',
		errorPolicy: 'ignore'
	},
	query: {
		fetchPolicy: 'no-cache',
		errorPolicy: 'all'
	}
}

ผลกระทบ: เปลี่ยนหน้าไหนก็ตาม ทุก query ดึงใหม่จาก server — ทำให้รู้สึกช้าเวลาเปลี่ยนหน้า โดยเฉพาะ query ที่ไม่ควรเปลี่ยนบ่อย (เช่น dropdown master data)

การลดความเสี่ยง: เปลี่ยน fetchPolicy เฉพาะ query ที่ควร cache เช่น:

js
this.props.client.query({
  query: Queries.dropdownListProvince,
  fetchPolicy: 'cache-first',
  variables: { ... }
})

P-3 · SiderLayout query profile ทุกครั้งที่ reload

SiderLayout ดึง profile + role flag ทุกครั้งที่เข้าหน้าใหม่ — ทำให้หน้าแรกช้ากว่าที่ควร

การลดความเสี่ยง: cache profile ใน localStorage + revalidate เป็นระยะ (stale-while-revalidate pattern)

P-4 · ไม่มี lazy loading ของรูป

รูปใน src/assets/images/ ไม่ได้ lazy load — ทุกรูปโหลดทันทีตอน page render

การลดความเสี่ยง: ใช้ loading="lazy" ใน <img> สำหรับรูปที่ไม่ได้อยู่ใน viewport แรก

P-5 · Webpack 4 (CRA ejected) ไม่มี webpack 5 features

webpack 5 มี ModuleFederationPlugin และ asset optimization ที่ดีกว่า — แต่ upgrade เป็น breaking change

P-6 · console.log กระจายอยู่ในหลายไฟล์

แม้ App/index.js:15 จะปิด console ไว้ แต่ console.log ยังอยู่ใน source code หลายสิบจุด — ทำให้ bundle ใหญ่กว่าที่ควร

การลดความเสี่ยง: ใช้ terser drop_console ใน webpack config เพื่อ strip ออกตอน build

P-7 · PDF.js โหลด worker แยกต่างหาก

pdfjs-dist ใช้ web worker แยก — โหลดเพิ่มอีก 1 request ทำให้หน้า Slip ช้ากว่าหน้าอื่น


การจัดลำดับความสำคัญ

PriorityปัญหาEffortผลกระทบ
สูงS-1 token ใน localStorageสูงมากลด XSS impact
สูงS-3 AES key hardcodedต่ำลด credential theft
สูงS-4 ไม่มี HTML sanitizerต่ำลด XSS
กลางP-1 bundle ใหญ่สูงเร่งเวลาโหลด
กลางS-6 console overrideต่ำdebug ง่ายขึ้น
กลางP-2 no-cache ทุก queryกลางเร่งเปลี่ยนหน้า
ต่ำS-7 GA ID เปิดเผยยอมรับได้
ต่ำP-7 PDF workerกลางเร่งหน้า Slip

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

ไป บท 6.10 Scorecard + known issues