Private Docs

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 cookiecookie ที่ 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 สำคัญ — ไม่รบกวนการใช้งานปกติ

อ่านต่อ