Frontend Hardening 101 — จุดอ่อนร่วมของทุกชั้น และวิธีปิด
อธิบายแบบง่ายที่สุด: ทำไม crypto 3 ชั้นถึงแพ้ XSS เหมือนกันหมด, XSS เข้ามาทางไหนได้บ้าง, มาตรการปิดทีละตัว (CSP, Trusted Types, supply chain, WebAuthn) และถ้าเกิดจริงอะไรเสียหาย-อะไรรอด — พร้อม sequence diagram
อัปเดต: 2026-08-04
Frontend Hardening 101 — จุดอ่อนร่วมของทุกชั้น และวิธีปิด
เล่มที่ 3 ของชุด 101 — Body Signature 101 ตอบเรื่อง “กันปลอม” · Encryption Field 101 ตอบเรื่อง “กันอ่าน” · เล่มนี้ตอบคำถามที่เหลือ: แล้วอะไรที่ทั้งสองชั้นกันไม่ได้ — และเราปิดมันยังไง
คำถามที่ 1: ทำไม crypto ตั้ง 3 ชั้น ถึงมีจุดอ่อนร่วมจุดเดียวกัน
ทั้ง 3 ชั้น (cookie / ลายเซ็น / เข้ารหัส) เก่งเรื่องเดียวกันหมด: กันภัยจาก “นอกเครื่อง” — ขโมย cookie ไปเครื่องอื่น, แก้ข้อมูลกลางทาง, ดักอ่านที่ข้อต่อ
แต่ทั้งหมดตั้งอยู่บนสมมติฐานเดียวกันโดยไม่รู้ตัว:
“โค้ดที่รันอยู่ในหน้าเว็บ = โค้ดของเรา”
XSS (Cross-Site Scripting) คือการทำลายสมมติฐานข้อนี้ — สคริปต์แปลกปลอมแอบเข้ามารันในหน้าเว็บเราได้สำเร็จ และเมื่อมันรันแล้ว browser มองไม่ออกเลยว่าใครเป็นใคร: สคริปต์ร้ายมีสิทธิ์เท่ากับโค้ดเราทุกประการ
ผลคือ crypto ทุกชั้นถูก “ใช้งานแทน” ได้ — ไม่ใช่ถูกเจาะ แต่ถูกสั่งงาน:
| สคริปต์ร้ายที่รันในหน้าเรา | ทำได้ไหม | เพราะอะไร |
|---|---|---|
| ขโมย private key ออกไปเครื่องอื่น | ❌ ไม่ได้ | ตู้เซฟ non-extractable — ไม่มีคำสั่งให้อ่านค่า |
| สั่งตู้เซฟ “เซ็นให้หน่อย” | ✅ ได้ | browser แยกไม่ออกว่าใครเรียก — สิทธิ์เท่าโค้ดเรา |
| อ่านค่า cookie ตรงๆ | ❌ ไม่ได้ (HttpOnly) | browser ไม่ยอมให้ JavaScript แตะ cookie ประเภทนี้ |
| ยิง request จากหน้านั้น — cookie แนบไปเองอัตโนมัติ | ✅ ได้ | ไม่ต้องอ่าน cookie ก็ใช้ session ได้ เพราะ browser แนบให้ทุก request อยู่แล้ว |
| ขโมยกุญแจเข้ารหัส | ❌ ไม่ได้ | browser ถือแต่ public key ซึ่งไม่ใช่ความลับ |
| อ่านข้อมูลบนหน้าจอ ก่อนถูกเข้ารหัส | ✅ ได้ | ข้อมูลที่ user กรอกอยู่ใน DOM — สคริปต์ในหน้าอ่านได้หมด |
สังเกต pattern: ของลับขโมยไม่ได้สักอย่าง แต่ “ใช้แทน” ได้ทุกอย่าง — ตราบใดที่เหยื่อยังเปิดหน้านั้นอยู่ · นี่คือเหตุผลที่ต้องมีเล่มนี้: กันไม่ให้สคริปต์ร้ายเข้ามารันได้ตั้งแต่แรก
คำถามที่ 2: สคริปต์ร้ายเข้ามาได้ทางไหน — มี 3 ประตู
graph TB
subgraph doors["3 ประตูที่ XSS ใช้เข้า"]
D1["ประตู 1 — ข้อมูลกลายเป็นโค้ด<br/>ข้อมูลที่ user อื่นกรอก (เช่น ชื่อบริษัท)<br/>ถูกเอาไปแสดงบนหน้าจอแบบดิบๆ<br/>ถ้าข้างในมี script แปะมา = ได้รัน"]
D2["ประตู 2 — โค้ดเราเองประกอบ HTML พลาด<br/>เช่นเอา input ไปต่อ string<br/>แล้วยัดเข้า innerHTML ตรงๆ"]
D3["ประตู 3 — ของที่เรา import มาโดนฝัง<br/>npm library ที่ใช้ถูก hack<br/>โค้ดร้ายแถมมากับ dependency"]
end
D1 --> X["สคริปต์ร้ายได้รันในหน้าเรา<br/>= สิทธิ์เท่าโค้ดเราทันที"]
D2 --> X
D3 --> X
ข่าวดีข้อแรก: Angular ปิดประตู 1 ให้เกือบหมดโดย default — การแสดงผลผ่าน template ({{ }}, property binding) ถูก escape อัตโนมัติ ข้อความที่มี script จะถูกแสดงเป็นตัวหนังสือไม่ใช่โค้ด · ประตูนี้จะเปิดก็ต่อเมื่อเราจงใจ bypass (เช่น bypassSecurityTrustHtml, innerHTML กับของดิบ) — ซึ่งคือประตู 2 นั่นเอง
คำถามที่ 3: ปิดยังไง — มาตรการทีละตัว เรียงตามลำดับความสำคัญ
3.1 CSP แบบเข้มงวด (nonce-based) — ปิดที่ต้นเหตุ: “สคริปต์ที่ไม่ได้รับอนุญาต ห้ามรัน”
หลักการง่ายมาก: server แจกรหัสผ่านประจำการโหลดหน้าแต่ละครั้ง (เรียกว่า nonce — สุ่มใหม่ทุกครั้ง) แล้ว browser จะรันเฉพาะสคริปต์ที่มีรหัสนี้แปะอยู่เท่านั้น
sequenceDiagram
participant B as Browser
participant S as Server (ที่เสิร์ฟหน้าเว็บ)
B->>S: ขอหน้าเว็บ
S->>S: สุ่มรหัสประจำรอบ เช่น nonce = "k9x2..."
S-->>B: ส่งหน้า + กฎ CSP ("รันได้เฉพาะสคริปต์ที่มีรหัส k9x2...")<br/>+ สคริปต์ของเราทุกตัวแปะรหัส k9x2... ไว้แล้ว
Note over B: สคริปต์ของเรา — มีรหัสถูกต้อง
B->>B: ✅ รันตามปกติ
Note over B: สคริปต์ที่ถูกฉีดเข้ามา (ประตู 1/2)<br/>— คนฉีดไม่มีทางรู้รหัสของรอบนี้
B->>B: ❌ browser ปฏิเสธ ไม่รันเลย + รายงานกลับ server
- ทำไม attacker ไม่รู้รหัส? — เพราะ nonce สุ่มใหม่ทุกการโหลดหน้า และสคริปต์ที่ฉีดเข้ามาทีหลังไม่ได้อยู่ในจังหวะที่ server แปะรหัสให้
- ของแถมที่สำคัญ: CSP มีโหมด report-only — เปิดแบบ “ยังไม่บล็อก แค่รายงาน” ก่อน เพื่อเก็บว่ามีสคริปต์ไหนบ้างที่จะโดนบล็อก แล้วค่อยเปิดบังคับจริง (แนวเดียวกับ log-only ของ body signature — ไม่มีการเปิดสวิตช์แบบเสี่ยงพังทั้งเว็บ)
3.2 Trusted Types — ตาข่ายชั้นสองสำหรับประตู 2
กฎที่บอก browser ว่า: “ห้ามยัด string ดิบเข้าจุดอันตรายของ DOM” (innerHTML และผองเพื่อน) — จะยัดได้ต้องผ่าน “โรงงานแปรรูป” (policy) ที่เราประกาศไว้เท่านั้น
ผล: ต่อให้โค้ดเราเผลอเขียน element.innerHTML = userInput — browser โยน error ทันทีตั้งแต่ตอน dev ไม่ใช่กลายเป็นช่องโหว่เงียบๆ ใน production · เปลี่ยนบั๊กประเภท “ตรวจไม่เจอจนโดนเจาะ” ให้กลายเป็น “พังตั้งแต่ตอนเขียน” ซึ่งดีกว่ามาก
3.3 Supply chain — เฝ้าประตู 3
ประตูนี้ crypto และ CSP ช่วยได้จำกัด (โค้ดร้ายที่มากับ library = โค้ด “ของเรา” ในสายตา browser ได้ nonce ไปด้วย) — ต้องกันตั้งแต่ตอนรับของเข้าบ้าน:
- SBOM + Dependency-Track — สแกนทุก dependency ใน CI ว่ามีช่องโหว่/ถูกฝังของไหม (วางระบบใน CI ของ FE แล้ว)
- lockfile — ล็อกเวอร์ชันทุกตัว ไม่ให้ version ใหม่ที่อาจโดน hack ไหลเข้ามาเองเงียบๆ
- ลดจำนวน dependency — ทุก package ที่ import คือประตูเพิ่มหนึ่งบาน ของที่เขียนเองได้ใน 20 บรรทัดไม่ต้อง import
3.4 WebAuthn step-up — ด่านสุดท้ายสำหรับ operation มูลค่าสูง
ทุกมาตรการข้างบนลดโอกาส XSS เกิด แต่ไม่มีอะไร 100% — สำหรับ operation ที่พลาดไม่ได้จริงๆ (โอนเงินก้อนใหญ่, เปลี่ยนข้อมูลบัญชี) มี primitive เดียวที่ XSS สั่งเงียบๆ ไม่ได้เลย: ลายเซ็นที่ต้องมีการกดของมนุษย์จริง
sequenceDiagram
participant X as สคริปต์ร้าย (XSS สำเร็จแล้ว)
participant B as Browser
participant U as ผู้ใช้ตัวจริง
participant S as Server
X->>B: แอบสั่ง "โอนเงิน 1 ล้านไปบัญชี attacker"
B->>S: request ไปถึง server
S-->>B: operation นี้ต้องมี WebAuthn — ขอลายเซ็นจาก authenticator
B->>U: 🔔 เด้งหน้าต่างระบบ: "แตะลายนิ้วมือ / กด security key เพื่อยืนยัน"
Note over U: หน้าต่างนี้เป็นของ OS ไม่ใช่ของหน้าเว็บ<br/>สคริปต์ในหน้าแตะต้องไม่ได้ กดแทนไม่ได้
U->>U: ไม่ได้กดโอนเอง — ไม่กด / กดยกเลิก
B-->>S: ไม่มีลายเซ็น
S-->>X: ❌ ปฏิเสธ — เงินไม่ออก
จุดที่ทำให้มันรอด: หน้าต่างยืนยันเป็นของระบบปฏิบัติการ ไม่ใช่ของหน้าเว็บ — สคริปต์ในหน้าไม่มีทางกดแทน และผู้ใช้ที่ไม่ได้กดโอนเองย่อมเอะใจว่าทำไมมีหน้าต่างเด้ง
3.5 หลักการที่ถืออยู่แล้ว — จำกัดความเสียหาย
ของที่ architecture ทำไว้ตั้งแต่ต้นและช่วยที่สุดในวันที่ XSS เกิดจริง: หน้าบ้านไม่มี secret ให้ขโมย — private key ล้วงไม่ได้, token จริงอยู่ server (cookie เป็นแค่ตัวชี้), ไม่มี API key/subscription key ใน bundle → สคริปต์ร้ายทำอะไรได้แค่ช่วงที่เหยื่อเปิดหน้า เอาอะไรติดมือออกไปใช้ต่อที่เครื่องอื่นไม่ได้เลย
คำถามที่ 4: สมมติ XSS เกิดจริง — อะไรเสียหาย อะไรรอด
ตารางนี้คือเหตุผลว่าทำไมทุกชั้นถึงยังคุ้มแม้กันตัว XSS เองไม่ได้:
| ระบบแบบเก่า (token/secret ใน browser) | ระบบเรา (3 ชั้น + hardening) | |
|---|---|---|
| ขโมย credential ออกไปใช้เครื่องอื่น | ✅ ได้ — ขโมย token ครั้งเดียว ใช้ได้ยาวจากที่ไหนก็ได้ | ❌ ไม่ได้ — ไม่มีอะไรให้ขโมย (non-extractable + token อยู่ server) |
| ช่วงเวลาก่อความเสียหาย | ยาว — จนกว่า token หมดอายุ/ถูก revoke | สั้น — เฉพาะตอนเหยื่อเปิดหน้าเว็บอยู่เท่านั้น ปิด tab = จบ |
| สั่งงานแทนเหยื่อระหว่างเปิดหน้า | ✅ ได้ | ✅ ยังได้ (จุดที่ CSP/Trusted Types ต้องมากัน) — ยกเว้น operation ที่ครอบ WebAuthn |
| อ่านข้อมูลบนหน้าจอ | ✅ ได้ | ✅ ยังได้ — ลดผลด้วยการ mask ข้อมูลที่ไม่จำเป็นต้องแสดงเต็ม |
| ตรวจจับ | ยาก — attacker ใช้ token จากเครื่องตัวเองเงียบๆ | ง่ายขึ้น — ความพยายาม register key ทับ = alert, CSP report = เห็นการฉีดตั้งแต่ยังไม่สำเร็จ |
คำถามที่ 5: ของเราวันนี้ยืนอยู่ตรงไหนแล้ว
| มาตรการ | สถานะ | ลำดับ |
|---|---|---|
| Angular auto-escape (ประตู 1) | ✅ ได้ฟรีจาก framework อยู่แล้ว | — |
| ไม่มี secret ใน bundle (ข้อ 3.5) | ✅ เป็นหลักของ architecture อยู่แล้ว (ตรวจโค้ดยืนยัน 03/08) | — |
| Supply-chain scan (SBOM + Dependency-Track) | 🔧 วางระบบใน CI แล้ว อยู่ระหว่าง rollout ให้ครบทุก FE repo | ทำต่อให้จบ |
| CSP nonce-based | ⏳ ยังไม่เริ่ม — เป็น track แยกใน security uplift (IMPL_PLAN_10) | ตัวถัดไปที่ควรเริ่ม — เปิด report-only ก่อนได้เลยไม่เสี่ยงอะไร |
| Trusted Types | ⏳ ยังไม่เริ่ม — ไปด้วยกันกับ CSP | ทำคู่ CSP |
| WebAuthn step-up | ⏳ อนาคต — รอ scope ว่า operation ไหนบ้างนับเป็น “มูลค่าสูง” | หลังชั้น 2+3 ลงตัว |
ภาพรวมจำง่ายๆ: crypto 3 ชั้น = กันภัยนอกเครื่อง · เล่มนี้ทั้งเล่ม = กันภัยในหน้าเว็บเอง · ต้องมีครบทั้งสองด้านถึงเรียกว่าหน้าบ้านปลอดภัยจริง — ด้านแรกออกแบบเสร็จแล้ว (รอ implement) ด้านหลังมีของฟรีจาก framework + supply-chain scan แล้ว เหลือ CSP/Trusted Types เป็นชิ้นใหญ่ถัดไป
ศัพท์ที่เจอบ่อย
| ศัพท์ | แปลว่า |
|---|---|
| XSS (Cross-Site Scripting) | สคริปต์แปลกปลอมได้เข้ามารันในหน้าเว็บเรา — สิทธิ์เท่าโค้ดเราทันที |
| DOM | โครงสร้างหน้าเว็บที่ JavaScript อ่าน/แก้ได้ — ข้อมูลบนหน้าจออยู่ในนี้ |
| innerHTML / DOM sink | จุดอันตรายที่ string กลายเป็นโค้ดได้ — ประตู 2 ของ XSS |
| HttpOnly cookie | cookie ที่ JavaScript อ่านไม่ได้ — browser แนบให้ตอนยิง request เท่านั้น |
| CSP (Content Security Policy) | กฎที่ server ประกาศว่า browser รันสคริปต์อะไรได้บ้าง |
| nonce | รหัสสุ่มประจำการโหลดหน้าแต่ละครั้ง — สคริปต์ไม่มีรหัส = ไม่ได้รัน |
| report-only | โหมดซ้อมของ CSP — ยังไม่บล็อก แค่รายงานว่าอะไรจะโดน |
| Trusted Types | กฎห้ามยัด string ดิบเข้า DOM sink — เปลี่ยนช่องโหว่เงียบให้เป็น error ตอน dev |
| SBOM | บัญชีรายชื่อ dependency ทั้งหมดที่ใช้ — เอาไว้ให้เครื่องมือสแกนหาช่องโหว่ |
| supply chain attack | โค้ดร้ายแอบมากับ library ที่เรา import — ประตู 3 |
| WebAuthn | ลายเซ็นที่ต้องมีการกดของมนุษย์จริงผ่านหน้าต่างของ OS — XSS กดแทนไม่ได้ |
| step-up | การขอยืนยันเพิ่มเฉพาะ operation สำคัญ — ไม่รบกวนการใช้งานปกติ |
อ่านต่อ
- Body Signature 101 — ลายเซ็นกันปลอมยังไง ทำไมขโมย key ไม่ได้
- Encryption Field 101 — การเข้ารหัส field กันอ่านยังไง ทั้งขาไปและขากลับ
- ภาพระบบ 3 ชั้น (Target Architecture) — ภาพใหญ่ที่ทุกเล่ม 101 ชี้กลับไป