Private Docs

IMPL PLAN 10 — Frontend Static Client (Angular บน Cloudflare)

มุมมอง client-side: secret ห้ามอยู่ใน static bundle — subscription key/signature/encryption ฝั่ง Angular ต้องออกแบบยังไง

อัปเดต: 2026-07-07

IMPL PLAN 10 — Frontend Static Client (Angular 21 บน Cloudflare)

วันที่: 07/07/2026 · สถานะ: ⏳ รอ verify · Priority: 🟠 คู่กับ Phase 1-2 อ้างอิง: IMPL_PLAN_02 §1/§3/§4 (กลไก key/signature/encryption) · IMPL_PLAN_03 §3.1 (subscription key ฝั่ง APIM) · DISCUSSION หมวด 2-3

เอกสารนี้ตอบอะไร

แผน 02/03 ตอบ “กลไกคืออะไร ฝั่ง server ทำอะไร” แล้ว — เล่มนี้ตอบมุมกลับ: client เป็น Angular 21 build เป็น static HTML/JS วางบน Cloudflare (ไม่มี server-side runtime ของตัวเอง) จะรองรับ 3 requirement ใหม่ได้ยังไง และต้องตัดสินใจอะไรบ้างก่อนลงมือ

หลักการตั้งต้น (จำให้ขึ้นใจก่อนหารือ):

ทุก byte ใน static bundle = public — ใครก็ view-source/devtools ได้ ดังนั้นของที่ฝังใน bundle ได้มีแค่ 2 ชนิด: (ก) ของที่เป็น public โดยนิยาม (RSA public key, API base URL) และ (ข) ของที่ยอมรับว่าเห็นได้เพราะไม่ใช่เกราะ (subscription key = app identity) ความลับจริงทุกตัวต้องมาจาก server หลัง authenticate (session-bound key) หรือไม่ต้องมีเลย (asymmetric)


§1 สรุป posture ที่แนะนำ ต่อ requirement (ตารางตัดสินใจสำหรับที่ประชุม)

RequirementPosture ที่แนะนำสำหรับ static clientSecret ใน bundle?Decision ที่ต้องเคาะ
Subscription keyฝังผ่าน runtime config (config.json — ดู §2) แนบทุก request ผ่าน interceptor ที่มีอยู่ (gateway-credentials.interceptor.ts) — ยอมรับว่า user เห็นได้ เพราะหน้าที่มันคือ identify app เพื่อ rate-limit/analytics ไม่ใช่ authentication✅ เห็นได้ (by design)D10-1: ยอมรับ key โผล่ใน devtools หรือจะซ่อนด้วย Cloudflare Worker (§5)?
Body signaturePhase 1: ไม่ sign จาก browser — ขา browser→APIM ใช้ TLS + HttpOnly cookie + CSRF + subscription key (ตาม IMPL_PLAN_02 §1 ทาง (1)) · HMAC บังคับเฉพาะ service-to-service/external partner ที่เก็บ key ใน KV ได้ · Phase 2 (optional): session-bound ephemeral key จาก Sentinel (IMPL_PLAN_02 §3) เมื่อที่ประชุมตัดสินว่าต้องกัน cookie-theft เพิ่ม❌ Phase 1 ไม่มี · Phase 2 อยู่ memory เท่านั้นD10-2: requirement “body signature” ของผู้ตรวจหมายรวมขา browser ด้วยไหม หรือ scope ที่ external/S2S พอ? — ตัวนี้ชี้ขาดว่า Phase 2 จำเป็นหรือไม่
Encryption fieldขา FE→BE: RSA-OAEP public key ผ่าน Web Crypto — เฉพาะ field อ่อนไหวสูงตาม PII inventory (เลขบัตร, KYC) ไม่ใช่ทั้ง body · ขา BE→FE: ไม่เข้ารหัส — minimization + mask ที่ server (IMPL_PLAN_02 §4.3)✅ แต่เป็น public key = ปลอดภัยโดยนิยามD10-3: list field ที่บังคับ encrypt (รอ PII inventory จาก IMPL_PLAN_08)
Report rendering (client วาดเอง)Client render ได้ แต่ backend เป็นผู้คุม dataset scope เสมอ (report-specific DTO ไม่ใช่ entity เต็ม) + tamper-evidence ฝั่ง server — รายละเอียด §8 และ S12— (ปัญหาคือ data exposure ไม่ใช่ secret)D10-4: report ตัวไหนถือเป็น “หลักฐาน” → ต้องมี server-side canonical copy/hash (= D13 ของ S12)

ประเด็นที่ต้องสื่อสารกับผู้ตั้ง requirement: ถ้า requirement เขียนว่า “client ต้อง sign body ด้วย shared secret” แบบตายตัว — สถาปัตยกรรม static SPA ทำแบบนั้นอย่างปลอดภัยไม่ได้ (secret จะกลายเป็น public ทันที) ทางที่เทียบเท่าหรือดีกว่าคือ session-bound key (D10-2) — ต้องเจรจา scope กับผู้ตรวจด้วยเหตุผลนี้ ไม่ใช่ฝืน implement แบบ security theater


§2 ปัญหาเฉพาะ static hosting: Config ต่อ Environment มาจากไหน

Static HTML deploy ครั้งเดียวเสิร์ฟทุกคน — ไม่มี server-side template ที่จะฉีดค่า per-env ตอน request ดังนั้นต้องเลือกวิธี inject config (subscription key, API base, RSA public key kid):

ทางวิธีข้อดีข้อเสีย
(A) Build per envenvironment.dev.ts / .sit.ts / .uat.ts / .prod.tsค่า bake เข้า bundle ตอน CI buildง่าย ตรงไปตรงมาartifact ต่างกันต่อ env = สิ่งที่เทสบน UAT ≠ สิ่งที่ขึ้น PROD (ขัดหลัก build-once) · rotate key = ต้อง rebuild
(B) Runtime configconfig.json ข้าง index.html, fetch ตอน APP_INITIALIZER/provideAppInitializerartifact เดียวทุก env · CI วาง config.json ต่างกันตอน deploy · rotate key = แก้ไฟล์เดียว ไม่ rebuildแนะนำ — สอดคล้อง build-once-deploy-manyต้องตั้ง Cache-Control: no-cache (หรือ short max-age + ETag) บน config.json มิฉะนั้น Cloudflare cache ค่าเก่าตอน rotate
(C) Cloudflare Worker injectWorker แนบ header/rewrite config ตอน serveซ่อน key จาก bundle ได้บางส่วนเพิ่ม moving part — ดู §5 ว่าคุ้มไหม

งานถ้าเลือก (B):

#จุดAction
1Frontend_HostAppSuperApp/apps/shell/เพิ่ม public/config.json + AppConfigService โหลดผ่าน provideAppInitializer ก่อน bootstrap
2gateway-credentials.interceptor.tsอ่าน Ocp-Apim-Subscription-Key จาก AppConfigService แทน hardcode/environment.ts
3CI/CD pipeline ของ frontendstep วาง config.json per env ตอน deploy ไป Cloudflare (ค่ามาจาก variable group — ห้าม commit ค่าจริงลง git แม้จะเป็น “แค่” subscription key ก็ rotate ยาก)
4Cloudflareตั้ง cache rule: config.json → no-cache/short TTL · asset อื่น hash-based cache ตามเดิม

§3 Sequence Diagram — Client Bootstrap → ใช้งานครบ 3 กลไก

sequenceDiagram
    autonumber
    participant B as Browser (Angular 21 SPA)
    participant CF as Cloudflare (static + WAF)
    participant APIM as APIM
    participant SG as Sentinel
    participant BE as Backend

    Note over B,CF: ① Bootstrap (ทุกครั้งที่เปิด app)
    B->>CF: GET /index.html + bundle (cached, hashed)
    B->>CF: GET /config.json (no-cache)
    CF-->>B: { apiBase, subscriptionKey, cryptoKeyEndpoint }

    Note over B,SG: ② Login (flow เดิม — OIDC ผ่าน Sentinel)
    B->>SG: /sentinel/login → OIDC PKCE
    SG-->>B: Set-Cookie HttpOnly<br/>(+ Phase 2: signingKey ใน /sentinel/user → เก็บ memory เท่านั้น)

    Note over B,BE: ③ เรียก API ปกติ
    B->>B: interceptor แนบ Ocp-Apim-Subscription-Key<br/>(+ Phase 2: X-Signature/X-Timestamp ผ่าน Web Crypto HMAC)
    B->>APIM: POST /api/... + Cookie + headers
    APIM->>APIM: validate subscription key → rate limit → resolve-session → validate-jwt
    APIM->>BE: forward + JWT

    Note over B,BE: ④ Form ที่มี field อ่อนไหว (ตาม PII inventory)
    B->>BE: GET /api/v1/crypto/public-key (cache ตาม kid)
    B->>B: crypto.subtle.encrypt(RSA-OAEP, publicKey, citizenId)
    B->>APIM: POST { citizenId: "rsa:<kid>:<b64>", ... }
    BE->>BE: decrypt ผ่าน KV Keys API (private key ไม่ออกจาก vault)
    BE-->>B: 200

§4 Activity Diagram — เลือกกลไกต่อ request ยังไง

flowchart TD
    A[Request จาก Angular SPA] --> B{มี field อ่อนไหวสูง<br/>ตาม PII inventory?}
    B -- ใช่ --> C[เข้ารหัส field นั้นด้วย RSA-OAEP<br/>ก่อนใส่ body]
    B -- ไม่ --> D[body ปกติ — TLS ครอบ]
    C --> E{Phase 2 เปิดแล้ว +<br/>เป็น write operation?}
    D --> E
    E -- ใช่ --> F[HMAC sign ด้วย session key<br/>จาก memory → X-Signature/X-Timestamp]
    E -- ไม่ --> G[ไม่ sign]
    F --> H[interceptor แนบ subscription key<br/>+ cookie ไปเองอัตโนมัติ]
    G --> H
    H --> I[ส่งผ่าน Cloudflare → APIM]

    style C fill:#e6f0ff
    style F fill:#fff3e0

§5 ทางเลือก: Cloudflare Worker เป็น thin proxy — คุ้มไหม

คำถามที่จะโดนถามแน่: “ในเมื่อ host บน Cloudflare อยู่แล้ว ใช้ Worker ซ่อน subscription key ได้ไหม?”

ไม่ใช้ Worker (แนะนำ)ใช้ Worker แนบ key
Subscription key ใน devtoolsเห็นไม่เห็น (Worker แนบให้หลัง edge)
ความปลอดภัยที่ได้จริงเท่ากัน — key ไม่ใช่เกราะ auth อยู่แล้ว (เกราะจริง = JWT + cookie + WAF)เท่ากัน + ตัดคนที่ copy key ไปยิงตรง APIM โดยไม่ผ่าน Cloudflare (แต่ APIM rate-limit ต่อ key ครอบเคสนี้อยู่แล้ว)
ต้นทุนศูนย์Worker = อีก 1 component ที่ต้อง version/monitor/secure · เพิ่ม latency hop · ทีมยังไม่มี Worker ใน stack
ผลต่อผู้ตรวจต้องอธิบายว่า key เป็น app identity ไม่ใช่ secret (มีเหตุผลรองรับใน IMPL_PLAN_03 §3.1)ตอบง่ายขึ้นผิวเผิน แต่เพิ่ม attack surface ใหม่ให้ตรวจ

ข้อเสนอ: ไม่ใช้ Worker ใน phase นี้ — เก็บเป็น fallback หากผู้ตรวจยืนยันว่า key ห้ามปรากฏใน client เท่านั้น (บันทึกเป็น decision D10-1)


§6 ข้อจำกัด runtime ฝั่ง browser ที่ต้องแจ้งทีมล่วงหน้า

ข้อจำกัดผลกระทบทางรับมือ
Web Crypto ใช้ได้เฉพาะ secure context (HTTPS/localhost)dev ผ่าน http IP → crypto ใช้ไม่ได้dev ใช้ localhost หรือ mkcert เสมอ
Session key (Phase 2) อยู่ memory → refresh หน้า = key หายต้อง re-fetch จาก /sentinel/user ทุก bootstrapมี flow อยู่แล้วใน IMPL_PLAN_02 §3 ③ — FE ต้อง handle race (request แรกอาจยิงก่อน key มา → interceptor ต้อง queue)
Multi-tab แชร์ session เดียว = key เดียวกันOK — แต่ละ tab fetch เองตอน bootstrapห้าม sync key ข้าม tab ผ่าน localStorage/BroadcastChannel (หลุด scope memory)
LIFF / mobile WebViewWeb Crypto + cookie behavior ต่างจาก browser ปกติเพิ่มใน test matrix ก่อนเปิด Phase 2 — ห้าม assume
CSP บน static hostingCSP header ต้องออกจาก Cloudflare (ไม่มี origin server ควบคุม)ตั้ง CSP ผ่าน Cloudflare response header rules + เก็บ config เป็น IaC/snapshot (คู่กับ IMPL_PLAN_03 §3.5 verify WAF)
Subresource Integrity (SRI)bundle โดน tamper ที่ CDN/edge = client รันโค้ดปลอมAngular build มี output hashing แล้ว — ประเมิน SRI เพิ่มเป็น nice-to-have (effort ต่ำถ้า index.html gen จาก CI)

§7 ลำดับงานฝั่ง Frontend (สอดคล้าง phase ของแผนหลัก)

ลำดับงานขึ้นกับPhase
1Runtime config (config.json + AppConfigService + cache rule)Phase 1
2แนบ subscription key ใน gateway-credentials.interceptor.ts1 + APIM สร้าง product/แจก key (IMPL_PLAN_03 §3.1ต้องแจกก่อน enforce)Phase 1
3crypto.service.ts (RSA-OAEP) + ใช้กับ form ตาม PII inventoryPII inventory (IMPL_PLAN_08) + BE endpoint /crypto/public-keyPhase 2
4body-signature.interceptor.ts (session-bound HMAC)D10-2 ตัดสินก่อน + Sentinel แจก key (IMPL_PLAN_02 §3)Phase 2+ (optional)
5CSP/SRI/cache audit บน Cloudflare + export config เป็นหลักฐานขนานได้
6Report client hardening (§8)Report authorization matrix + inventory (S12)Phase 2

§8 Report Client — เอา data มา render เอง ต้องระวังอะไร

Governance/backend control ของ use case นี้อยู่ครบใน S12 แล้ว — section นี้ตอบเฉพาะมุม static client + คำถาม “data จะถูกดัดแปลงไหม”

8.1 ตอบตรงเรื่อง tampering — แยก 3 ขา

ขาดัดแปลงได้ไหมเกราะ
ระหว่างทาง (server → browser)❌ ไม่ได้TLS (มีอยู่แล้ว) — ไม่ต้องทำอะไรเพิ่ม
บนเครื่อง user เอง (devtools แก้ DOM, แก้ตัวเลขก่อน print/screenshot)ได้เสมอ ป้องกันไม่ได้ — โค้ดและ data อยู่ในมือ user แล้วทำได้แค่ tamper-evidence: server เก็บ canonical copy + hash ของ dataset ต่อ report id → ใครเอา report ที่พิมพ์จาก client มาอ้าง ตรวจกับต้นฉบับฝั่ง server ได้ · report ที่เป็นหลักฐานทางธุรกิจ → server-side PDF generation แทน client-only render (decision D10-4/D13) + watermark (user, timestamp, report id)
ข้อมูลวนกลับ (client ส่งค่าที่คำนวณ/แก้แล้วกลับมา)✅ ถ้า backend เชื่อกฎเหล็ก: backend ห้าม trust ค่า derived จาก client — ยอดรวม/สถานะ/ผลคำนวณใดๆ ต้อง recompute ฝั่ง server เสมอ · client ส่งได้แค่ report type + filters แล้ว backend validate scope เอง

สรุปให้ที่ประชุม: ความเสี่ยงจริงของ report client ไม่ใช่ “ข้อมูลถูกแก้ระหว่างทาง” (TLS ปิดแล้ว) แต่คือ (ก) ส่ง data เกินจำเป็นไปให้ client และ (ข) ไม่มีต้นฉบับให้ตรวจเมื่อ report ถูกใช้เป็นหลักฐาน — สองข้อนี้แก้ที่ backend ไม่ใช่ที่ Angular

8.2 งานฝั่ง Angular (report page/component)

#งานเหตุผล
1เรียก report ผ่าน endpoint เฉพาะ (report type + filters) — ห้าม reuse endpoint CRUD ที่คืน entity เต็มแล้วซ่อน column ด้วย UIUI ซ่อน ≠ ปลอดภัย — devtools เห็น full payload (Excessive Data Exposure)
2ห้ามเก็บ report dataset ใน localStorage/sessionStorage/IndexedDB · state ใน memory เท่านั้น + clear เมื่อออกจากหน้า (report P0)static SPA ไม่มี server session — storage ฝั่ง browser คือจุดรั่วหลัก
3ห้ามใส่ filter ที่เป็น PII ใน route/query string (?citizenId=) — ส่งทาง POST bodyURL ติด browser history + Cloudflare/proxy log
4render ค่าอย่างเดียว — ห้ามคำนวณยอด/สถานะฝั่ง client แล้วส่งกลับขา 3 ของ 8.1
5download/export ใช้ short-lived opaque URL จาก backend — FE ไม่สร้าง link เอง ไม่ cache linklink leak = data leak (S12 §4)
6ระวัง print CSS / window.print() — ตรวจว่า mask แล้วจาก server ไม่ใช่ mask ด้วย CSS (display:none ยังอยู่ใน DOM)mask ฝั่ง client ถอดได้ด้วย devtools

8.3 สิ่งที่ backend ต้องมีให้ (dependency — ชี้ไป S12)

  • Report-specific DTO + permission check + tenant/user scope ต่อ report (S12 §1-2)
  • Cache-Control: no-store, private บน response ที่มี PII (S12 §3)
  • Audit ทุก report P0/P1: ใคร, report อะไร, filter อะไร (masked), กี่ record (S12 §5)
  • Canonical hash / server-side PDF สำหรับ report ที่เป็นหลักฐาน (S12 §6 + D10-4)

§9 หลักฐานจากการ scan โครงสร้างจริง (07/07/2026 — Frontend_HostAppSuperApp + Frontend_SuperAppLibraryUi)

ตอบข้อสงสัย “client หละหลวมไหม” — scan ด้วย grep pattern เบื้องต้น (ยังไม่ใช่ full audit) พบของจริงหลายจุด — ข้อสังเกตถูกต้อง มีช่องโหว่จริงให้ prove ได้ เรียงตามความรุนแรง:

#ไฟล์ (ตำแหน่ง ณ วัน scan)สิ่งที่พบระดับทางแก้
F1HostApp apps/shell/.../application-tracking/report-access-token.store.tsAccess token ของ report (fx.reportAccessToken) ถูก persist ลง localStorage — XSS ขโมยได้ + ค้างข้าม session/ปิด browser · สัมพันธ์ตรงกับ report use case §8🔴class มี in-memory field อยู่แล้ว — ตัดขา localStorage ทิ้ง (ยอม re-fetch หลัง refresh) หรือย้ายไปผูก session cookie ฝั่ง backend
F2HostApp .../core/msal-instance.ts:44MSAL cacheLocation: 'localStorage' — token/account cache อยู่ localStorage ถาวร🟠เปลี่ยนเป็น sessionStorage (MSAL รองรับ) — ลด blast radius · trade-off: เปิด tab ใหม่ต้อง silent re-auth → ทดสอบ UX ก่อน
F3HostApp .../consent/consent.service.ts:77console.log('[CookieConsent] Raw localStorage:', raw) — debug log ค้างใน production path🟡ลบ + เพิ่ม build-time strip console (esbuild drop: ['console'] หรือ lint rule no-console)
F4HostApp .../sidebar/sidebar.ts:82 + msal-auth.service.ts:159console.log('Auth State:', userProfile) และ log MsalUserInfo ทั้ง object — PII/profile ลง console🟠ลบ — โยง IMPL_PLAN_08 งาน 8.5 + CI grep gate
F5LibraryUi libs/ui-kit/.../text-editor-view.component.ts:61bypassSecurityTrustHtml(raw) โดย default sanitize=false — HTML จาก CMS/admin render ตรง → stored XSS ถ้า admin panel ถูก compromise หรือ content ถูก inject ทาง API🔴กลับ default เป็น sanitize=true — จุดที่ต้อง raw HTML จริงค่อย opt-out รายตัวพร้อมเหตุผล (หลัก “admin content = trusted” พังทันทีเมื่อ admin โดน XSS/CSRF เสียเอง)
F6LibraryUi[innerHTML] หลายจุด: consent-modal (purpose.description จาก API), service-tc-consent (admin/fx), modal-input subtitlebinding [innerHTML] ผ่าน Angular sanitizer โดย default (กัน script ได้) — แต่เป็น surface ที่โตขึ้นเรื่อยๆ🟡audit ว่าไม่มีจุดไหนส่งผ่าน bypass + บังคับ CSP เป็นเกราะชั้นที่สอง (§6)
F7LibraryUi README.md:949เอกสารสอน pattern localStorage.setItem(TOKEN_KEY, token) — ชี้ทางผิดให้ dev คนถัดไป🟡แก้ README — เอกสารคือ template ของโค้ดในอนาคต
F8HostApp apps/shell/src/environments/environments.prod.tsconfig bake ตอน build (ทาง (A) ของ §2) + มี TODO ค้าง (clientId: '')สนับสนุนการย้ายไปทาง (B) runtime config — และ fail-fast ตอน bootstrap ถ้า config บังคับว่าง

สิ่งที่ยังไม่ได้ตรวจ (อย่า assume ว่าผ่าน): CSP header จริงบน Cloudflare · console strip ใน production build config · dependency vulnerability (รอ Dependency-Track จาก IMPL_PLAN_01) · remote MF apps อื่น (ดูเฉพาะ shell + LibraryUi)

งานที่เพิ่มเข้า §7: F1/F5 = แก้ทันที (Phase 0 ของฝั่ง FE) · F2-F4, F7 = Phase 1 · CI grep gate (localStorage.*token|console\.log.*user|bypassSecurityTrust) เข้า regression gates ของ IMPL_PLAN_01 A9

Verify

  • config.json เปลี่ยนค่า → app รับค่าใหม่โดยไม่ rebuild (ทดสอบ rotate subscription key)
  • devtools network: ทุก API request มี Ocp-Apim-Subscription-Key · ไม่มี key → APIM ตอบ 401 (ไม่ถึง backend)
  • form อ่อนไหว: network tab เห็น rsa:<kid>:... ไม่เห็น plaintext
  • (Phase 2) refresh หน้า → request แรกหลัง bootstrap ยังมี signature ถูกต้อง (ไม่มี race)
  • grep -rn "localStorage\|sessionStorage" ใน shell — ไม่มี key/token/PII ถูกเก็บ
  • Cloudflare response headers มี CSP ครบ + config.json ไม่ติด long-cache
  • Report: devtools network เห็นเฉพาะ field ที่หน้าจอใช้ (ไม่มี entity เต็ม) · response header Cache-Control: no-store · route/query ไม่มี PII
  • Report: user A เปลี่ยน report id/filter เป็นของ user B → 403 (negative test ตาม S9)

⚠️ ข้อควรระวัง

  1. ห้าม commit subscription key ค่าจริงลง git — แม้เป็น app identity การ rotate จะยากถ้าฝังใน history (บทเรียนเดียวกับ IMPL_PLAN_06)
  2. ลำดับ enforce subscription key: แจก key ให้ทุก client (web/mobile/partner) + monitor ว่า traffic มี key ครบ → ค่อยเปิดบังคับที่ APIM — เปิดก่อน = client เดิมพังหมด (IMPL_PLAN_03 ข้อระวัง #1)
  3. อย่าเสนอ “เข้ารหัส response ให้ browser” ในที่ประชุม — เป็น security theater (IMPL_PLAN_02 §4.3) ถ้าผู้ตรวจขอ ให้ตอบด้วย minimization + masking
  4. Angular 21: ใช้ provideAppInitializer (functional) ไม่ใช่ APP_INITIALIZER token แบบเก่า — สอดคล้อง standalone/signals convention ของ repo

🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)

  1. แก้ F1 (report token ใน localStorage) ก่อนทุกอย่าง — เป็น diff เล็ก (ตัด 3 บรรทัด) แต่ปิดช่อง XSS-steal-token ที่ชัดที่สุดใน repo — ไม่ต้องรอที่ประชุม
  2. เลือกทาง (B) runtime config — build-once คือวินัยที่จะขอบคุณตอน rotate subscription key ครั้งแรก · ไม่ใช้ Cloudflare Worker — เพิ่ม component โดยไม่เพิ่มความปลอดภัยจริง
  3. Body signature ขา browser: เจรจา scope ก่อน implement — ผมจะไม่ทำ session-bound key (§3 ของ PLAN 02) จนกว่าผู้ตรวจจะยืนยันเป็นลายลักษณ์อักษรว่า scope ครอบ browser — effort สูง ผลตอบแทนต่ำเมื่อเทียบ TLS+cookie+CSRF ที่มีอยู่
  4. RSA field encryption ทำเฉพาะ form P0 จริงๆ (KYC, เลขบัตร) — เริ่มหลัง PII inventory ของ UserService เสร็จ อย่าทำก่อนมี list
  5. F5 (text-editor sanitize=false) ผมจะ flip default เป็น true ทันที แม้จะ breaking — ให้จุดที่เสีย formatting opt-out เอง ดีกว่าปล่อย stored-XSS surface เปิดไว้ default
  6. Report client: สิ่งเดียวที่ผมจะลงทุนเกิน checklist §8 คือ report-specific DTO — เพราะ excessive data exposure เป็นความเสี่ยงจริงที่เกิดแล้วเกิดอีก ส่วน tampering ฝั่ง client เป็นความเสี่ยงที่รับได้ถ้า backend ไม่ trust derived value
  7. ลำดับที่ผมจะทำ: F1+F5 → runtime config → subscription key interceptor → console/log hygiene (F3/F4) → RSA encryption → (ถ้าจำเป็น) session-bound signature

📚 อ้างอิงมาตรฐาน (สำหรับใช้ในที่ประชุม):

หลักการแหล่งอ้างอิง
ห้ามเก็บ sensitive data/token ใน localStorage (F1/F2)OWASP HTML5 Security Cheat Sheet — Local Storage
Token สำหรับ browser app — ควรอยู่ฝั่ง server (BFF) ไม่ใช่ JS-accessible storageIETF — OAuth 2.0 for Browser-Based Applications (draft BCP)
Angular sanitization + อันตรายของ bypassSecurityTrust* (F5/F6) — officialAngular — Security Best Practices
XSS prevention + output encodingOWASP XSS Prevention Cheat Sheet
CSP เป็นเกราะชั้นสองกัน XSSOWASP Content Security Policy Cheat Sheet · MDN — Content Security Policy
MSAL.js cache location trade-offs (F2) — official repoMSAL.js — Caching in MSAL Browser
Web Crypto API (RSA-OAEP/HMAC ฝั่ง browser)MDN — Web Crypto API
Cache-Control: no-store สำหรับ response ที่มี PII/reportMDN — Cache-Control