recruitment-web — Scorecard
Legend:
- Critical = active security/data-loss/availability risk
- High = correctness/maintainability hazard, real impact
- Medium = friction/hygiene, ยังไม่เป็นอันตราย
repo นี้ (branch: prod) รับข้อมูลส่วนบุคคลของผู้สมัครงานจริงจำนวนมาก (เลขบัตรประชาชน, ที่อยู่, เบอร์โทร, เงินเดือน ฯลฯ) และมีปัญหาการจัดการ secret/PII หลายจุดที่กระทบกันเป็นลูกโซ่
Scorecard table
| ID | Severity | คำอธิบาย | หลักฐาน (file:line) | การแก้ไข |
|---|---|---|---|---|
| SEC-RCWEB-01 | Critical | .env, .env.dev, .env.uat ถูก track ใน git และมีค่า REACT_APP_SECRET_KEY / REACT_APP_PRIVATE_KEY จริง — คีย์ใช้ AES ฝั่ง client (utils/cryptoJs.js) CRA bake REACT_APP_* เข้า JS bundle ดังนั้นคีย์เปิดเผยทั้งใน git history และใน bundle ที่ดาวน์โหลดจากเบราว์เซอร์ได้ การ "เข้ารหัส" จึงไม่ปกป้องจากผู้ที่อ่าน bundle/repo ได้ Dockerfile stage-2 ยังฝัง REACT_APP_SECRET_KEY อีกชุด (Dockerfile:15) ซึ่งไม่มีผลต่อ bundle แต่เพิ่มพื้นผิวการรั่ว | .env (branch: prod), .env.uat, src/utils/cryptoJs.js:1-32, Dockerfile:15 | ย้าย encrypt/decrypt PII ไปฝั่ง recruitment-api เท่านั้น; หมุนคีย์ที่รั่วทั้งหมด; ลบ .env* ที่มี secret ออกจาก git history; อย่าใส่ secret ใน Dockerfile |
| SEC-RCWEB-02 | High | PII ที่ decrypt แล้ว (idCard, ที่อยู่, โทร, วันเกิด, เงินเดือน) เก็บใน localStorage key user-login โดยไม่มีวันหมดอายุ — expiry ใน setStorageWithExpity / getStorageWithExpiry ถูก comment ปิดทั้งสองด้าน | src/utils/localStorage.js:1-28 (บรรทัด 6-7, 19-26) | เปิด expiry จริง หรือย้าย PII ที่ decrypt แล้วไป in-memory (Recoil) ลดพื้นผิว XSS/shared device |
| SEC-RCWEB-03 | High | handleLogin ข้าม backend OTP และใช้ OTP คงที่ 111111 เมื่อ REACT_APP_NODE_ENV === 'dev' — เป็น runtime check ใน production bundle ได้ถ้า build ผิด env | src/components/header/index.js:230-231 | ลบ bypass จาก client; mock OTP ที่ backend/mock server เท่านั้น |
| SEC-RCWEB-04 | Medium | อีเมลและ OTP ส่งผ่าน query string ของ GET (validate_otp) และอีเมลผ่าน logout query — ถูกบันทึกใน access log / browser history | src/pages/login/form-otp.js:51-52, src/components/header/index.js:74,232,266 | เปลี่ยนเป็น POST + body สำหรับข้อมูลระบุตัวตน/OTP |
| CORR-RCWEB-01 | Medium | Dockerfile ตั้ง ENV REACT_APP_* ที่ stage-2 (nginx) แต่ CRA inline ค่าตอน stage-1 build เท่านั้น — ค่า stage-2 ไม่มีผล ทำให้เข้าใจผิดว่า config มาจากไหน | Dockerfile:1-21 | ลบ ENV ที่ stage-2 หรือใช้ ARG ที่ stage-1 ให้ตรงพฤติกรรม CRA |
| QUAL-RCWEB-01 | Medium | src/api/login.js + src/api/index.js เป็น dead code — ไม่มี import จาก UI; ใช้ axios client คนละตัวและ endpoint คนละชุด | src/api/login.js:1-53, src/api/index.js:1-6 | ลบ src/api/ และ src/utils/axios.js หลังยืนยันไม่มีแผนใช้ |
| QUAL-RCWEB-02 | Medium | utils/axios.js ตั้งชื่อพารามิเตอร์ interceptor ผิด (response ทั้งที่เป็น request config), ตั้ง Authorization: null และ Access-Control-Allow-Origin: '*' เป็น request header ฝั่ง client (ไม่มีผลกับ CORS) — path นี้เป็น dead code | src/utils/axios.js:8-15 | ลบพร้อม QUAL-RCWEB-01 |
| QUAL-RCWEB-03 | Medium | typo env REACT_APP_RECAPTCHA_SITE_KET (ควรเป็น KEY) สอดคล้องกันทั้ง .env*, และจุดอ่านใน content.js — แก้จุดเดียวจะพัง | .env (key name), content.js:1355 | แก้ typo พร้อมกันทุกจุดแล้ว deploy พร้อมกัน |
| QUAL-RCWEB-04 | Medium | logic decrypt PII ~25+ ฟิลด์ซ้ำใน header/index.js และ content.js — เพิ่ม field ต้องแก้ 2 จุด | header/index.js:104-148, content.js:492-537 | ดึงเป็น decryptApplicantProfile(data) shared utility |
| LEG-RCWEB-01 | Medium | ไฟล์แนบแปลงเป็น base64 ฝัง JSON แทน multipart; ตรวจ type/ขนาด 2MB เฉพาะ client ในสโคป repo นี้ | uploadForm.js:22-30, content.js:654-668,677-696 | ตรวจซ้ำที่ recruitment-api (task แยก — UNVERIFIED จากมุม frontend) |
สรุปลำดับแก้
- SEC-RCWEB-01 — secret ใน git + client AES (กระทบความน่าเชื่อถือของการเข้ารหัสทั้งระบบ)
- SEC-RCWEB-02 — PII ค้างใน localStorage
- SEC-RCWEB-03 — OTP bypass ใน bundle
- SEC-RCWEB-04 — OTP ใน GET query
- hygiene: CORR / QUAL / LEG ตามตาราง
สิ่งที่ตรวจแล้วว่าไม่ใช่ปัญหาคนละชนิด
| หัวข้อ | สรุป | Citation |
|---|---|---|
| Refresh token | มี interceptor จริงบน 401/403 | axios-rc.js:52-72 |
| Route gate | มี soft gate สำหรับ MyJob / ApplicantProfile | App.js:49-62 |
| GA | initialize ด้วย env GA_ID; mute console นอก UAT/local | App.js:29-34 |
| nginx SPA | try_files ถูกต้องสำหรับ CRA | default.conf:7-11 |
Cross-ref
- OTP + AES detail → 02-core-pipeline
- ทุกหน้า / ทุก endpoint → 03-pages-and-endpoints
- โครงสร้างไฟล์ → 01-repository-map