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 (ตารางตัดสินใจสำหรับที่ประชุม)
| Requirement | Posture ที่แนะนำสำหรับ static client | Secret ใน 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 signature | Phase 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 env — environment.dev.ts / .sit.ts / .uat.ts / .prod.ts | ค่า bake เข้า bundle ตอน CI build | ง่าย ตรงไปตรงมา | artifact ต่างกันต่อ env = สิ่งที่เทสบน UAT ≠ สิ่งที่ขึ้น PROD (ขัดหลัก build-once) · rotate key = ต้อง rebuild |
(B) Runtime config — config.json ข้าง index.html, fetch ตอน APP_INITIALIZER/provideAppInitializer | artifact เดียวทุก 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 inject | Worker แนบ header/rewrite config ตอน serve | ซ่อน key จาก bundle ได้บางส่วน | เพิ่ม moving part — ดู §5 ว่าคุ้มไหม |
งานถ้าเลือก (B):
| # | จุด | Action |
|---|---|---|
| 1 | Frontend_HostAppSuperApp/apps/shell/ | เพิ่ม public/config.json + AppConfigService โหลดผ่าน provideAppInitializer ก่อน bootstrap |
| 2 | gateway-credentials.interceptor.ts | อ่าน Ocp-Apim-Subscription-Key จาก AppConfigService แทน hardcode/environment.ts |
| 3 | CI/CD pipeline ของ frontend | step วาง config.json per env ตอน deploy ไป Cloudflare (ค่ามาจาก variable group — ห้าม commit ค่าจริงลง git แม้จะเป็น “แค่” subscription key ก็ rotate ยาก) |
| 4 | Cloudflare | ตั้ง 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 WebView | Web Crypto + cookie behavior ต่างจาก browser ปกติ | เพิ่มใน test matrix ก่อนเปิด Phase 2 — ห้าม assume |
| CSP บน static hosting | CSP 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 |
|---|---|---|---|
| 1 | Runtime config (config.json + AppConfigService + cache rule) | — | Phase 1 |
| 2 | แนบ subscription key ใน gateway-credentials.interceptor.ts | 1 + APIM สร้าง product/แจก key (IMPL_PLAN_03 §3.1 — ต้องแจกก่อน enforce) | Phase 1 |
| 3 | crypto.service.ts (RSA-OAEP) + ใช้กับ form ตาม PII inventory | PII inventory (IMPL_PLAN_08) + BE endpoint /crypto/public-key | Phase 2 |
| 4 | body-signature.interceptor.ts (session-bound HMAC) | D10-2 ตัดสินก่อน + Sentinel แจก key (IMPL_PLAN_02 §3) | Phase 2+ (optional) |
| 5 | CSP/SRI/cache audit บน Cloudflare + export config เป็นหลักฐาน | — | ขนานได้ |
| 6 | Report 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 ด้วย UI | UI ซ่อน ≠ ปลอดภัย — 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 body | URL ติด browser history + Cloudflare/proxy log |
| 4 | render ค่าอย่างเดียว — ห้ามคำนวณยอด/สถานะฝั่ง client แล้วส่งกลับ | ขา 3 ของ 8.1 |
| 5 | download/export ใช้ short-lived opaque URL จาก backend — FE ไม่สร้าง link เอง ไม่ cache link | link 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) | สิ่งที่พบ | ระดับ | ทางแก้ |
|---|---|---|---|---|
| F1 | HostApp apps/shell/.../application-tracking/report-access-token.store.ts | Access token ของ report (fx.reportAccessToken) ถูก persist ลง localStorage — XSS ขโมยได้ + ค้างข้าม session/ปิด browser · สัมพันธ์ตรงกับ report use case §8 | 🔴 | class มี in-memory field อยู่แล้ว — ตัดขา localStorage ทิ้ง (ยอม re-fetch หลัง refresh) หรือย้ายไปผูก session cookie ฝั่ง backend |
| F2 | HostApp .../core/msal-instance.ts:44 | MSAL cacheLocation: 'localStorage' — token/account cache อยู่ localStorage ถาวร | 🟠 | เปลี่ยนเป็น sessionStorage (MSAL รองรับ) — ลด blast radius · trade-off: เปิด tab ใหม่ต้อง silent re-auth → ทดสอบ UX ก่อน |
| F3 | HostApp .../consent/consent.service.ts:77 | console.log('[CookieConsent] Raw localStorage:', raw) — debug log ค้างใน production path | 🟡 | ลบ + เพิ่ม build-time strip console (esbuild drop: ['console'] หรือ lint rule no-console) |
| F4 | HostApp .../sidebar/sidebar.ts:82 + msal-auth.service.ts:159 | console.log('Auth State:', userProfile) และ log MsalUserInfo ทั้ง object — PII/profile ลง console | 🟠 | ลบ — โยง IMPL_PLAN_08 งาน 8.5 + CI grep gate |
| F5 | LibraryUi libs/ui-kit/.../text-editor-view.component.ts:61 | bypassSecurityTrustHtml(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 เสียเอง) |
| F6 | LibraryUi — [innerHTML] หลายจุด: consent-modal (purpose.description จาก API), service-tc-consent (admin/fx), modal-input subtitle | binding [innerHTML] ผ่าน Angular sanitizer โดย default (กัน script ได้) — แต่เป็น surface ที่โตขึ้นเรื่อยๆ | 🟡 | audit ว่าไม่มีจุดไหนส่งผ่าน bypass + บังคับ CSP เป็นเกราะชั้นที่สอง (§6) |
| F7 | LibraryUi README.md:949 | เอกสารสอน pattern localStorage.setItem(TOKEN_KEY, token) — ชี้ทางผิดให้ dev คนถัดไป | 🟡 | แก้ README — เอกสารคือ template ของโค้ดในอนาคต |
| F8 | HostApp apps/shell/src/environments/environments.prod.ts | config 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)
⚠️ ข้อควรระวัง
- ห้าม commit subscription key ค่าจริงลง git — แม้เป็น app identity การ rotate จะยากถ้าฝังใน history (บทเรียนเดียวกับ IMPL_PLAN_06)
- ลำดับ enforce subscription key: แจก key ให้ทุก client (web/mobile/partner) + monitor ว่า traffic มี key ครบ → ค่อยเปิดบังคับที่ APIM — เปิดก่อน = client เดิมพังหมด (IMPL_PLAN_03 ข้อระวัง #1)
- อย่าเสนอ “เข้ารหัส response ให้ browser” ในที่ประชุม — เป็น security theater (IMPL_PLAN_02 §4.3) ถ้าผู้ตรวจขอ ให้ตอบด้วย minimization + masking
- Angular 21: ใช้
provideAppInitializer(functional) ไม่ใช่APP_INITIALIZERtoken แบบเก่า — สอดคล้อง standalone/signals convention ของ repo
🎯 ถ้าเป็นผมจะทำอย่างไร (ความเห็น Fable)
- แก้ F1 (report token ใน localStorage) ก่อนทุกอย่าง — เป็น diff เล็ก (ตัด 3 บรรทัด) แต่ปิดช่อง XSS-steal-token ที่ชัดที่สุดใน repo — ไม่ต้องรอที่ประชุม
- เลือกทาง (B) runtime config — build-once คือวินัยที่จะขอบคุณตอน rotate subscription key ครั้งแรก · ไม่ใช้ Cloudflare Worker — เพิ่ม component โดยไม่เพิ่มความปลอดภัยจริง
- Body signature ขา browser: เจรจา scope ก่อน implement — ผมจะไม่ทำ session-bound key (§3 ของ PLAN 02) จนกว่าผู้ตรวจจะยืนยันเป็นลายลักษณ์อักษรว่า scope ครอบ browser — effort สูง ผลตอบแทนต่ำเมื่อเทียบ TLS+cookie+CSRF ที่มีอยู่
- RSA field encryption ทำเฉพาะ form P0 จริงๆ (KYC, เลขบัตร) — เริ่มหลัง PII inventory ของ UserService เสร็จ อย่าทำก่อนมี list
- F5 (text-editor sanitize=false) ผมจะ flip default เป็น true ทันที แม้จะ breaking — ให้จุดที่เสีย formatting opt-out เอง ดีกว่าปล่อย stored-XSS surface เปิดไว้ default
- Report client: สิ่งเดียวที่ผมจะลงทุนเกิน checklist §8 คือ report-specific DTO — เพราะ excessive data exposure เป็นความเสี่ยงจริงที่เกิดแล้วเกิดอีก ส่วน tampering ฝั่ง client เป็นความเสี่ยงที่รับได้ถ้า backend ไม่ trust derived value
- ลำดับที่ผมจะทำ: 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 storage | IETF — OAuth 2.0 for Browser-Based Applications (draft BCP) |
Angular sanitization + อันตรายของ bypassSecurityTrust* (F5/F6) — official | Angular — Security Best Practices |
| XSS prevention + output encoding | OWASP XSS Prevention Cheat Sheet |
| CSP เป็นเกราะชั้นสองกัน XSS | OWASP Content Security Policy Cheat Sheet · MDN — Content Security Policy |
| MSAL.js cache location trade-offs (F2) — official repo | MSAL.js — Caching in MSAL Browser |
| Web Crypto API (RSA-OAEP/HMAC ฝั่ง browser) | MDN — Web Crypto API |
Cache-Control: no-store สำหรับ response ที่มี PII/report | MDN — Cache-Control |