#history#xss#redirect#web-security
ภาพรวม
ช่องโหว่ฝั่งเว็บของ Next.js ชุด S3-S5 พบในรอบตรวจความปลอดภัย 2026-10-24 (faa6e6d0) เป็นช่องที่ฟีเจอร์ใหม่เปิดเอง: returnTo จาก 6ba1a6d9, popup แผนที่, proxy รูปที่ผู้ใช้อัปโหลด ทั้งหมดอยู่นอกขอบ backend (frontend/src, frontend/shared) แต่เก็บไว้เป็นบทเรียนของกลุ่มความปลอดภัย
อาการ
- W1 open redirect:
/login?returnTo=...และ/select-database?returnTo=...ส่งผู้ใช้ไปเว็บภายนอกหลังล็อกอินได้ (ลิงก์ฟิชชิงบนโดเมนจริง) - W2 stored XSS: ชื่อ/ที่อยู่ลูกค้าจากฐานข้อมูลถูกใส่ popup แผนที่ที่ Leaflet ตีความเป็น HTML
- W3 ชนิดไฟล์อันตราย: เปิดลิงก์รูปที่ proxy ตรง ๆ แล้วไฟล์
text/htmlหรือimage/svg+xmlที่ผู้ใช้อัปโหลดรันสคริปต์บนโดเมนของเรา (ตามคอมเมนต์ในsafeMediaContentType) - W4 wildcard ใน LIKE: ผู้ใช้พิมพ์
%,_,\ในช่องค้นหาลูกค้า (BuySaleInformation) แล้วถูกตีเป็น wildcard ผลค้นหาผิด - W5 (ไม่ใช่ช่องโหว่):
localStorageถูกบล็อก (Private/นโยบายเบราว์เซอร์) แล้ว throw ทำจอ master ทั้งจอพัง
สาเหตุ (root cause)
- W1: 6ba1a6d9 รับ
returnToตรวจแค่startsWith("/") && !startsWith("//")แต่เบราว์เซอร์ตีความ/\hostเป็น//hostและ control character ทะลุได้ - W2: ข้อมูลจาก DB ต่อสตริง HTML ส่ง
bindPopupโดยไม่ escape - W3: proxy ส่งต่อ
Content-Typeของต้นทางทั้งหมดและไม่ตั้งnosniff - W4: ใช้
escapeSqlกัน quote อย่างเดียว ไม่ได้ escape ตัวอักษรพิเศษของ LIKE - W5: เรียก
localStorageนอก try/catch
วิธีแก้
- W1: helper
safeReturnPath(raw, fallback="/home")ยอมเฉพาะ path ภายใน: ขึ้นต้น/, ไม่ขึ้นต้น//หรือ/\, ไม่มี\และ control character; มีเทสต์safe-return.test.tsการแก้แยกสองคอมมิต: helper อยู่ faa6e6d0 แต่ถูกเรียกใช้ที่หน้า login/select-database ใน 386e3baa (ชื่อ commit "premium design layer") ห่างกัน 15 วินาที - W2:
escapePopupHtmlแทน& < > " 'ก่อนใส่ popup - W3:
safeMediaContentTypeยอมเฉพาะimage/(png|jpeg|gif|webp|bmp|avif|x-icon)กับapplication/pdfนอกนั้นเป็นapplication/octet-streamพร้อมX-Content-Type-Options: nosniffที่ route รูป crm, docpicture, itempicture - W4: 6cb904f7 ใส่
\นำหน้า\ % _แล้วESCAPE '\'ต่อท้ายILIKE - W5: ครอบ
try/catchในจอ master 6 จอ (ค่าเริ่มต้น true)
commit ที่เกี่ยวข้อง
6ba1a6d9— เพิ่มreturnTo(ต้นเหตุ W1; 2026-10-20)faa6e6d0—safeReturnPath,escapePopupHtml,safeMediaContentType(2026-10-24)386e3baa— นำsafeReturnPathมาใช้ในหน้า login/select-database แทนเช็กเดิม (2026-10-24)6cb904f7— escape LIKE และlocalStorageปลอดภัย (2026-10-24)
โค้ดปัจจุบันที่ป้องกัน
frontend/src/lib/safe-return.ts:6safeReturnPath; ใช้ที่src/app/login/page.tsx:87,src/app/select-database/page.tsx:250; เทสต์src/lib/safe-return.test.tsfrontend/shared/ui/SmlMapPickerModal.tsx:185escapePopupHtmlใช้ที่:742-743frontend/src/lib/api-server.ts:42safeMediaContentType;src/app/api/crm/image/route.ts:62,68;process/docpicture/image/route.ts:35-36;process/itempicture/image/route.ts:35-36frontend/account/report/BuySaleInformationScreen.tsx:64-67;frontend/account/master/CustomerScreen.tsx:113-118(try/catch)- ช่องว่างจากการอ่านโค้ด (ไม่ใช่ประวัติ):
ILIKEอยู่ใน 7 ไฟล์ frontend แต่มีเพียง BuySaleInformation ที่ใส่ESCAPE(ยังไม่ได้ตรวจว่าไฟล์อื่น escapekwก่อนหรือไม่); ผลของ wildcard คือผลค้นหาผิด ไม่ใช่การฉีด SQL
บทเรียน / กฎที่ได้
- ค่าที่จะเป็น URL ปลายทางต้องผ่านตัวตรวจกลางตัวเดียว (allow-list ของ path ภายใน) ไม่เช็กเองทีละหน้า
- ข้อมูลจาก DB ที่ใส่ HTML (popup, innerHTML) ต้อง escape ที่จุดต่อสตริงเสมอ
- proxy ไฟล์ของผู้ใช้ต้อง allow-list
Content-Type+nosniffห้ามส่งต่อค่าต้นทาง - escape ให้ตรงบริบท: quote ของ SQL กับ wildcard ของ LIKE เป็นคนละเรื่อง
- ตัวช่วยความปลอดภัยต้องมีทั้ง "ฟังก์ชัน" และ "การเรียกใช้" ในที่เดียวกัน ตรวจด้วย grep ว่ามีผู้เรียกจริง
ดูเพิ่ม
เช็กลิสต์ความปลอดภัย API: ตัวตน สิทธิ์ และช่องโหว่เว็บ (Secure API Checklist) · บทเรียนบั๊กสิทธิ์และการข้ามฐานข้อมูล (Auth Bypass, Cross-Tenant, Demo Read-Only, Screen Rights) · บทเรียนตัวตนผู้เช่า การล็อกอิน และ session (Tenant, Auth & Sessions) · เส้นทาง API ฝั่ง Next.js ที่ส่งต่อไป Go backend (API proxy routes) · ตัวเรียก API และตัวประกอบ SQL ฝั่งเบราว์เซอร์ (api-client, query-client, sml-query-builder) · history-overview
ที่มาในโค้ด
- git:6ba1a6d9, git:faa6e6d0, git:386e3baa, git:6cb904f7
- frontend/src/lib/safe-return.ts:6
- frontend/shared/ui/SmlMapPickerModal.tsx:185
- frontend/src/lib/api-server.ts:42