Private Docs

Handoff — Master Plan fact-check รอบ 2

สถานะรอบ fact-check ที่ 2 ของ master plan

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

Handoff — Security Master Plan: รอบ fact-check ที่ 2 ยังไม่ได้ apply

บันทึกไว้เมื่อ: 2 กรกฎาคม 2026 · เอกสารเป้าหมาย: SECURITY_MASTER_PLAN_DEFENSE_ROADMAP.md (แผนแม่บทความปลอดภัย SuperApp)

สถานะ: มี fact-check review ผ่านมาแล้ว 2 รอบ — รอบแรก apply เสร็จแล้ว (6 จุด) รอบสอง ยังไม่ได้ apply เลยสักจุด (13 confirmed findings + 5 nits) นี่คืองานที่ค้างอยู่


บริบท: ทำไมมี 2 รอบ

  1. Session ก่อนหน้า (ครบัง crash ด้วย API error ระหว่างเรียก advisor()tool_use_id เสียใน transcript) เขียนแผนแม่บทเสร็จ + ยิง review workflow รอบแรก (7 agents: 3 reviewer มุมมองต่างกัน + adversarial verify ต่อ finding) ผลออกมาแล้วตอน session crash แต่ยังไม่ได้อ่าน
  2. Session นี้กู้ผล workflow รอบแรกจาก output file, apply ครบ 6 จุด (F1-F6), อัปเดต status บรรทัด 7 ของเอกสารเป็น “ผ่าน fact-check review แล้ว”
  3. User ขอให้ advisor() review อีกรอบ — advisor tool unavailable ตลอด session นี้ (ลองซ้ำ 2 ครั้ง ทั้งก่อนและหลังเปลี่ยน model เป็น opus/sonnet) จึงใช้ Workflow tool สร้าง multi-agent re-review แทน (4 reviewer: fix-verify, fresh-critic, code-facts, sec-advisor ใช้ agent type security-advisor) พร้อม adversarial verify ต่อ finding เหมือนรอบแรก
  4. Workflow รอบสอง (task id w660vncc0) เสร็จแล้ว: 13 confirmed findings (7 must_fix, 6 should_fix) + 5 nits — ยังไม่ได้ apply แม้แต่จุดเดียวตอน session ถูกขัดจังหวะ (user เปลี่ยน model 2 ครั้งแล้วพิมพ์ “handoff”)

หลักฐานเต็ม (ทุก finding มี file

+ verifyReason ละเอียด): C:\Users\SUPAKO~1\AppData\Local\Temp\claude\c--Source-Private-Atlas-docs\f5f43cb5-b403-4d94-9455-e4ce71afcbdf\tasks\w660vncc0.output (เป็น temp file — อาจถูกลบโดย OS ได้ สรุปย่อทุกจุดอยู่ด้านล่างนี้แล้ว ถ้าไฟล์หายให้ใช้สรุปย่อแทนได้เลย ไม่กระทบการทำงานต่อ)


งานที่ต้อง apply ต่อ (13 confirmed — เรียงตาม severity)

🔴 must_fix (7 จุด)

M1. Backend_WorkflowService หายจาก inventory bypass ทั้งฉบับ — bypass ร้ายแรงกว่าที่รายงาน

  • ตำแหน่งที่ต้องแก้: §0 ตาราง #2 (บรรทัด 33), §3.1 #2 (บรรทัด 193-201), §5 Phase 1 (บรรทัด 336)
  • ปัญหา: WorkFlow01.API/Program.cs:73-81 ลงทะเบียน BypassAuthHandler เป็น auth scheme เดียวในทุก env ที่ไม่ใช่ Production (guard เดียว = throw ถ้า IsProduction()) — ไม่ผูกกับ ASPNETCORE_ENVIRONMENT=Development เหมือน 9 service อื่น ดังนั้น hedge ที่เอกสารเพิ่งใส่ไป (“Critical ก็ต่อเมื่อ env=Development”) ใช้ไม่ได้กับ WF — WF bypass ติดทั้ง dev/sit/uat เสมอ BypassAuthHandler return success เสมอพร้อม claim Role=Admin โดยไม่ดู token เลย (แรงกว่า if-isDev เดิมที่อย่างน้อยยังต้องส่ง JWT-shaped token)
  • แก้: เพิ่ม WorkflowService เข้า inventory ทั้ง 3 จุด, ระบุว่าเป็น bypass คนละ class (scheme-replacement ไม่ใช่ if(isDev)), ตัดประโยค “UserService เป็นตัวเดียวที่ปิด”, แก้ Phase 1 verify criterion (grep if(isDev) = 0 ไม่จับ WF เพราะ WF ไม่มี pattern นี้อยู่แล้ว — ต้องมี verify แยกสำหรับ WF)

M2. §3.1 #3 (FallbackPolicy) เหมารวม endpoint ที่ตั้งใจเปิด anonymous เข้ากับ “ลืมติด [Authorize]”

  • ตำแหน่ง: §0 บรรทัด 34, §3.1 #3 (บรรทัด 206-213), §4.2 Step 5 (บรรทัด 295), §5 Phase 1 (บรรทัด 334)
  • ปัญหา: ThirdParty controllers (CustomerController, EGovTokenController, FileManagementController ฯลฯ) ติด [AllowAnonymous] explicit ระดับ class พร้อม write endpoints จริง — FallbackPolicy ช่วยอะไรไม่ได้เพราะ [AllowAnonymous] override เสมอ (พฤติกรรมมาตรฐาน ASP.NET Core) แต่ roadmap ไม่มี task ไหนสั่งถอด/แก้ endpoint พวกนี้เลย — Phase 1 verify คือ “ทุก controller มี [Authorize] หรือ [AllowAnonymous] ชัดเจน” ซึ่ง ThirdParty ผ่านอยู่แล้วโดยไม่ต้องแก้อะไร ทั้งที่ยังเปิด anonymous 100%
  • แก้: แยกเป็น 2 เรื่อง — (a) ไม่มี FallbackPolicy (เคส “ลืมติด” เช่น ConsentController) (b) [AllowAnonymous] explicit บน write endpoint ที่ต้อง audit รายตัวแล้วเปลี่ยนเป็น [Authorize]/[RequireApiKey] — เพิ่มงาน + verify per-endpoint (เรียกไม่มี credential ต้องได้ 401/403) เข้า Phase 1

M3. ConsentService rate-limit pattern ที่แผนสั่งให้ลอกเป็น non-partitioned — self-DoS risk

  • ตำแหน่ง: §3.1 #5 / §5 Phase 1 (rate-limit helper)
  • ปัญหา: AddFixedWindowLimiter("GlobalPolicy", 100/min) ของ ConsentService เป็น counter เดียวแชร์ทุก client ทั้ง service ไม่ใช่ per-client — ถ้า helper กลางลอก pattern นี้ตรงๆ: client เดียวยิง 100 req/min ก็กินโควตาหมด → user จริงทุกคนโดน 429 (self-DoS ง่ายกว่าเดิม) และเมื่อผสมกับ penalty layer (นับ 429 ต่อ identity) ผู้ใช้บริสุทธิ์ที่โดน 429 จาก global window จะถูกสะสมจน ban เอง
  • แก้: helper กลางต้องใช้ partitioned limiter (partition key = user sub → API key → client IP จาก forwarded header) ไม่ใช่ AddFixedWindowLimiter ตัวเดียวแชร์ทั้ง service — อย่าอ้าง Consent GlobalPolicy เป็น pattern ต้นแบบ, ระบุให้ exclude /health จาก limiter ด้วย

M4. RateLimitPenaltyMiddleware ไม่มี enforcement จริง — PenaltyBox.IsBanned ไม่มี caller ที่ไหนเลย

  • ตำแหน่ง: §3.1 #5 (คำอธิบายที่เพิ่งแก้ไปว่าเป็น “ban-escalation layer”)
  • ปัญหา: grep ทั้ง platform พบ PenaltyBox.IsBanned มีแค่ definition ไม่มีที่ไหนเรียกใช้ — wire middleware นี้แล้วได้ 0 การป้องกันจริง (ban ลง in-memory dict ที่ไม่มีใครอ่าน) ซ้ำ IdentityResolver เชื่อ header X-Demo-User ที่ client ปลอมได้ (spoof เพื่อ ban เหยื่อ) และ fallback เป็น Connection.RemoteIpAddress ซึ่งหลัง APIM/ingress คือ IP เดียวกันหมด (ban ครั้งเดียว = anonymous ทั้ง platform โดน); state เป็น per-pod in-memory ไม่ consistent บน AKS หลาย replica
  • แก้: อย่าใส่ RateLimitPenaltyMiddleware ใน helper กลางเวอร์ชันแรก — primary AddRateLimiter (partitioned ตาม M3) อย่างเดียวพอ ถ้าจะใช้ escalation จริงต้องเพิ่ม (1) middleware เช็ค IsBanned ก่อน next() (2) ตัด X-Demo-User ออกจาก IdentityResolver (3) ใช้ IP จาก forwarded headers ที่ผ่าน UseForwardedHeaders+KnownNetworks แล้วเท่านั้น (4) เก็บ state ใน Redis ไม่ใช่ in-memory

M5. DGA ConsumerSecret เป็น external API contract — ย้ายฝ่ายเดียวจะพัง token acquisition

  • ตำแหน่ง: §3.1 #4 / Phase 0 (ย้าย ConsumerSecret ไป header/body)
  • ปัญหา: URL ที่มี ?ConsumerSecret= เป็น request ขาออกไป DGA (api.egov.go.th/ws/auth/validate) — contract เป็นของ DGA ไม่ใช่ของเรา (ยืนยันจาก DGA API spec: query param เป็นข้อบังคับ ไม่มีทางเลือก POST body) ทำตามแผน “ย้ายไป header/body” ฝ่ายเดียว = token acquisition พังทันที ช่องรั่วจริงที่คุมได้คือฝั่ง log ของเราเอง — แต่ตรวจแล้วพบว่า .NET 9+ HttpClientFactory redact query string เป็น default อยู่แล้ว (ThirdParty target net10.0, ไม่มี opt-out) และโค้ดเรา log แค่ AuthUrl ไม่มี query อยู่แล้ว ดังนั้นความเสี่ยงที่เหลือคือ access log ฝั่ง DGA/TLS-inspecting proxy เท่านั้น ไม่ใช่ log ของเรา
  • บันทึกเพิ่มจาก verifier: ระหว่างตรวจพบ ConsumerKey/ConsumerSecret ค่า prod hardcode อยู่ใน appsettings.Production.json:9-11 ของ ThirdParty — ควรรวม scope นี้เข้ากับ Finding #1 (hardcoded credential) ด้วย
  • แก้: เปลี่ยน task เป็น (1) ยืนยัน DGA spec ว่า query param บังคับจริง (2) mitigation ที่คุมได้คือ Serilog override category System.Net.Http.HttpClient ให้ ≥Warning + ยืนยัน TLS-only/egress proxy ไม่เก็บ URL ไม่ใช่ “ย้ายไป header” (3) เพิ่ม ThirdParty prod hardcoded secret เข้า scope ของ Finding #1

M6. ApiKeyAuthFilter ไม่ได้ wire เข้า ConsentService — attribute จะเงียบสนิท

  • ตำแหน่ง: §4.2 Step 2 (pattern [AllowAnonymous]+[RequireApiKey])
  • ปัญหา: ApiKeyAuthFilter enforce ก็ต่อเมื่อ service register options.Filters.Add<ApiKeyAuthFilter>() เข้า MVC — ยืนยันแล้วว่า ConsentService เรียก AddApiKeyAuth แต่ไม่เคย add filter เข้า AddControllers (grep ทั้ง repo ไม่พบ) ในขณะที่ FilterService/Codex/Centralized/FileManagement/Log/Notification/Orchestrator/Template/Task/UserService register ครบ — ติด [AllowAnonymous]+[RequireApiKey] บน Consent วันนี้ = attribute ไม่มีผลอะไร endpoint เปิด anonymous เต็มรูปแบบ (Consent คือ service เป้าหมายของแผนพอดี — POST /receipt)
  • แก้: เพิ่ม prerequisite ใน Step 2 — ก่อนใช้ pattern นี้บน service ใดต้อง verify (1) Filters.Add<ApiKeyAuthFilter>() อยู่ใน AddControllers จริง (2) ApiKeyAuth:ApiKeys มีค่าใน config/KV ของ env นั้น (dict ว่าง = ปฏิเสธทุก key = fail-closed พังของจริง) — แนะนำให้ helper กลาง register filter อัตโนมัติ + fail-fast ตอน startup ถ้า ApiKeys ว่าง

M7. Backend_ThirdPartyFXService ไม่มี authentication เลยทั้ง repo — ไม่อยู่ใน inventory ของแผน

  • ตำแหน่ง: §4.2 Step 1-4 + §4.3 (FallbackPolicy rollout “ทุก service”)
  • ปัญหา: grep AddAuthentication|AddAuthorization|AddJwtBearer|FallbackPolicy ทั้ง repo = 0 hit, ไม่มี [Authorize] แม้แต่จุดเดียว, MapHub<FxRateHub>("/hubs/fx-rate") เปิด anonymous — เปิด FallbackPolicy ที่นี่โดยไม่มี auth scheme ลงทะเบียนก่อนจะได้ InvalidOperationException (“No authenticationScheme was specified…”) = 500 ทุก request ไม่ใช่ 401 (คนละ failure mode จาก service อื่น)
  • แก้: เพิ่ม Step 0 ใน §4.2 — ตรวจว่า service มี JWT authentication ลงทะเบียนจริงก่อนเข้า rollout (AddAppAuthentication) — service ที่ยังไม่มี (ThirdPartyFX, และ Workflow ด้วยเหตุผลคนละแบบ = “bypass ไม่ใช่ validation จริง” ไม่ใช่ “จะ 500”) ต้อง port auth ก่อน — เพิ่ม ThirdPartyFXService เข้า inventory ของแผน

🟡 should_fix (6 จุด)

S1. §3.1 #3 bullet วิธีแก้ (บรรทัด 213) ยังเป็น wording เก่าที่ขัดกับ §4.2 Step 2 ที่แก้แล้ว (health probe ติด attribute ไม่ได้ + ไม่มีหมวด API-key ใน inventory summary) → ปรับให้สอดคล้อง §4.2 ใหม่

S2. Over-claim ทั้งฉบับ — §3 intro (บรรทัด 183) + footer (บรรทัด 394) เขียน “ทุกข้อยืนยันจากโค้ดจริง” แต่มีอย่างน้อย 5 ข้อในเนื้อหาที่เอกสารเองบอกว่า “ต้อง verify แยก/ควร confirm” (Notification auth pattern, ShowPII scope, Swagger SIT/UAT, Sentinel InternalOnly IaC, RoleController) → เปลี่ยน claim เป็น “ข้อที่มี file

= ยืนยันแล้ว; ข้อ ⚠️/ควร confirm = ยังไม่ verified”

S3. HMAC/integrity gap วางผิดหมวดเทียบ guideline จริง — อยู่ใต้ §2 หมวด 4 (Secure Communication) แต่ตาม API-GL-001 ข้อ 3.2 Data Integrity (ใต้ข้อ 7(3)) ต่างหาก ที่พูดเรื่อง message-level integrity/JWS ตรงๆ ทำให้ §2 หมวด 4 ให้สถานะ ✅ กลบ severity 🟠 High ที่ §0 ให้ไว้ → ย้าย gap ไปหมวด 3, ปรับสถานะ/คะแนนให้สอดคล้อง

S4. Phase 1 แถว “เปลี่ยนทุก if(isDev) bypass” มี verify แค่ syntactic (grep = 0) ไม่มี functional check — ปิด bypass = behavior change จริง (เปิด JWT validation เต็มที่ dev cluster ไม่เคย exercise มาก่อน เช่น AzureAd config ที่อาจขาด/ผิด) เสี่ยงพัง dev/sit แต่ verify ผ่านได้ทั้งที่พัง → เพิ่ม functional verify เหมือนแถว FallbackPolicy (ทีละ service + E2E เขียว + valid token 200 / no token 401)

S5. ASPNETCORE_ENVIRONMENT ที่เอกสารทิ้งเป็นคำถามเปิด (“ต้องยืนยันจาก IaC”) จริงๆ ตอบได้จาก repo เองแล้ว — Dockerfile.DEV ทุก service bake ENV ASPNETCORE_ENVIRONMENT=Development + IaC dev deployment manifests ตั้งค่าเดียวกัน explicit ครบ (ยกเว้น sentinel-gateway ที่ไม่ตั้งแต่ image ENV ชนะอยู่ดี) → ยกระดับ #2 เป็น confirmed-Critical สำหรับ dev cluster ได้เลย (ไม่ใช่ conditional อีกต่อไป) พร้อม note SIT/UAT ใช้ชื่อ env Sit/Uat จริง (bypass แบบ if-isDev ไม่ active ที่นั่น ยกเว้น Workflow ที่ bypass ทุก non-Production)

S6. (ซ้ำเนื้อหากับ M1 จากมุม sec-advisor — ดูรายละเอียดเพิ่มเรื่อง comment ในโค้ด WF เองที่บอกว่ารอ “final phase” port JWT validation จริง)

🔵 nits (5 จุด — เอาไว้ทำทีหลังก็ได้)

  1. §3.1 #2 heading (บรรทัด 193) ยังพิมพ์ “เปิดจริง” ทั้งที่ Exec Summary แก้เป็น “อยู่ในโค้ดจริง” แล้ว
  2. §2 หมวด 1 บรรทัด 122 คำเดียวกัน (“เปิดจริง ~10 service”)
  3. §2 หมวด 2 “มาตรฐานสั่ง” ตกข้อกำหนด replay-attack protection (nonce/short-lived token) ของ guideline ข้อ 7(2)
  4. PDPA wording (§2 หมวด 3 + §5 Phase 3) ทิศทางคลุมเครือ — guideline จริงคือ “ฝ่ายที่ทำลายข้อมูลส่งหลักฐานมาให้ธนาคาร” ไม่ใช่เราส่งออก
  5. §4.2 Step 1 inventory ไม่ครอบ SignalR hub endpoints (MapHub) ซึ่งโดน FallbackPolicy เหมือน health probe + ต้องมี OnMessageReceived อ่าน access_token จาก query (NotificationService ทำแบบนี้อยู่แล้ว เป็นตัวอย่างได้)

สิ่งที่ทำไปแล้ว (ไม่ต้องทำซ้ำ)

  • รอบแรก (F1-F6) apply ครบแล้วจริง — ตรวจสอบได้จาก diff ของไฟล์ปัจจุบัน เทียบ workflow output wsqpebkm5.output (temp path เดิม, session ก่อน)
  • บรรทัด 7 ของเอกสาร (status line) อัปเดตแล้วว่า “ผ่าน fact-check review แล้ว (รอบแรก)” — ต้องอัปเดตอีกครั้งหลัง apply รอบสองเสร็จ ให้สะท้อนว่าผ่าน 2 รอบ

สิ่งที่ยังไม่ได้ทำ / ต้องระวัง

  • ทั้ง 13 confirmed + 5 nits ด้านบนยังไม่ได้แตะเอกสารเลย — เป็นงานหลักที่ค้าง
  • advisor() tool unavailable ตลอด session ที่ผ่านมา (ทั้งก่อนและหลังเปลี่ยน model) — ถ้า session ใหม่ยังเจอ error เดิม ให้ใช้ Workflow-based multi-agent re-review แทนเหมือนที่ทำมา 2 รอบแล้ว (ดู pattern ใน w660vncc0 — 4 reviewer + adversarial verify ต่อ finding)
  • COMPLIANCE_MATRIX.md ในโฟลเดอร์เดียวกัน (สร้างคนละ session, ไม่เกี่ยวกับงานนี้โดยตรง) link ไปหา SECURITY_SCAN_FINDINGS.md และ SECURITY_HARDENING_PLAN.md ที่ไม่มีอยู่จริงในระบบไฟล์เลย — น่าจะเป็น doc set ของ Sentinel Gateway (SG-13..SG-26) จากอีก session ที่ยังสร้างไม่ครบ ไม่ใช่ scope ของ handoff นี้ แต่ถ้า user ถามถึงให้แจ้งว่าไฟล์ที่ referenced ยังไม่มี
  • ไฟล์ทั้งหมดใน security/02072026/ ยัง untracked ใน git (ไม่เคย commit) — ถ้า apply เสร็จแล้ว ให้ถาม user ก่อน commit ตามปกติ

Suggested skills สำหรับ session ถัดไป

  • ไม่ต้องใช้ skill พิเศษ สำหรับงาน apply findings — เป็นการแก้ไฟล์ markdown ตรงไปตรงมาด้วย Edit tool ตาม list ด้านบน ทำตามลำดับ severity (must_fix ก่อน)
  • หลัง apply เสร็จครบ ถ้าต้องการความมั่นใจเพิ่มก่อนส่งให้ทีม ให้ลองเรียก advisor() อีกครั้งก่อน (อาจกลับมาใช้ได้แล้ว) — ถ้ายัง unavailable ให้ fallback เป็น Workflow re-review รอบ 3 แบบเดิม (เฉพาะจุดที่เพิ่งแก้ใหม่ ไม่ต้อง full re-review ทั้งฉบับอีก เพราะรอบ 1-2 cover ทั้งฉบับไปแล้ว)
  • ถ้า user ขอให้เขียน SECURITY_SCAN_FINDINGS.md / SECURITY_HARDENING_PLAN.md ที่ COMPLIANCE_MATRIX.md อ้างถึง (Sentinel Gateway scope, คนละเรื่องกับ master plan นี้) ให้เริ่มจากอ่าน sentinel-gateway-service/ โฟลเดอร์อื่นๆ ในภาพก่อน (มี SECURITY_REVIEW.md, SECURITY_CHECKLIST.md จาก 04062026 ที่อาจมี SG-ID เดิมให้ต่อยอด) — งานนี้แยกจาก handoff นี้โดยสิ้นเชิง

Reference

  • เอกสารเป้าหมาย: SECURITY_MASTER_PLAN_DEFENSE_ROADMAP.md
  • Workflow รอบแรก (F1-F6, apply แล้ว): task id wsqpebkm5
  • Workflow รอบสอง (13+5, ยังไม่ apply): task id w660vncc0, journal เต็ม: C:\Users\supakornp\.claude\projects\c--Source-Private-Atlas-docs\f5f43cb5-b403-4d94-9455-e4ce71afcbdf\subagents\workflows\wf_712622ca-ba5\journal.jsonl
  • ไม่มี hardcoded credential ค่าจริงใดๆ ถูกคัดลอกมาใน handoff นี้ — อ้างเฉพาะ file
    ตามที่เอกสารต้นทางทำไว้แล้ว