สมอง SML

คลังความรู้ / ประวัติ & บทเรียน

#history#query-gateway#security#lockdown

ภาพรวม

ในยุค ADR-0006 session ของผู้เช่าทุกคนส่ง INSERT/UPDATE/DELETE ถึงตารางธุรกิจใดก็ได้ผ่าน /query/exec จึงข้ามกฎ Go (rule 10) การล็อกทำเป็นขั้น 2026-10-21 .. 2026-10-24 และแต่ละขั้นมีทางลัดที่พบทีหลัง

อาการ

  • Q1 (239b0997): หน้าจอหรือคำขอทำมือเขียน ic_trans, gl_journal, ic_inventory.balance_qty ได้ตรง ไม่ผ่านการคำนวณ/ลงบัญชี/transaction ของ Go
  • Q2 (239b0997, 3ef39d32): level ต่ำเขียนตารางธุรกิจได้ทุกตาราง และ erp_user_datalist_config เก็บ SQL ที่ไปรันในเบราว์เซอร์ผู้ใช้คนอื่น
  • Q3 (04903202): route Next process/barcode/[action] มี fallbackBarcodeAction ยิง DELETE/INSERT/UPDATE ic_inventory_barcode ผ่าน /query/execBatch เองเมื่อ backend ตอบ 404
  • Q4 (a47413ec): /blob/insert รับชื่อตาราง+คอลัมน์จากผู้ใช้โดยไม่ผ่านกฎเดียวกับ /query/exec (ข้อความ commit)
  • Q5 (239b0997): SQL ที่ตัวตรวจอ่านเป้าหมายไม่ออก: MERGE, ชื่อ U&"...", CTE ที่ซ่อน write, คำสั่งไม่มีเป้าหมายชัด

สาเหตุ (root cause)

  • Q1/Q2: นโยบายเดิมคุมแค่ "เป็น DML หรือไม่" ไม่คุม "ตารางไหน ใครเขียน" มีเพียง ValidateUserLevel กับตารางระบบ 3 กลุ่ม
  • Q3: ทางสำรองฝั่ง Next เขียนตรงผ่าน gateway เป็นประตูหลังที่ Go ไม่เห็น (ผิด rule 9/10)
  • Q4: ประตูเขียนมีสองบาน กฎล็อกอยู่บานเดียว
  • Q5: เป้าหมายอ่านด้วย regex (writeTargetPattern) ไวยากรณ์ที่ regex อ่านไม่ได้ต้อง "ปฏิเสธ" ไม่ใช่ "ปล่อย"

วิธีแก้

  • Q1: handler เบราว์เซอร์ตั้ง ClientRequest=true แล้ว service เรียก ValidateClientPolicy(level); ตารางเงิน/สต็อก/GL ปฏิเสธทุกคน (403 ErrMoneyTableViaExec) และย้ายหน้าจอที่เขียนตารางเหล่านี้ไป /api/process ในคอมมิตเดียวกัน (stock_beginning, tax_amend, point_doc, doc_no_next)
  • Q2: level >= 9 เขียนตารางอื่นได้; level < 9 เขียนได้เฉพาะ legacyClientWritable กับ prefix monitormap_ นอกนั้น ErrAdminOnlyWrite หลัก "ratchet": ย้ายหน้าจอไป Go แล้วถอดตารางพร้อมเพิ่มเคสเทสต์ (3ef39d32 ถอด datalist config ไปเซฟผ่าน /process/master/datalistsave)
  • Q3: ลบ fallback ทิ้ง route เหลือ proxy 49 บรรทัด
  • Q4: เพิ่ม ValidateClientInsert(table, columns, level) ใช้ตารางและ regex คอลัมน์ชุดเดียวกัน พร้อมเทสต์ level 1 ได้ 403
  • Q5: MERGE ถูกปฏิเสธ, U&"..." และ "ไม่มีเป้าหมาย" เป็น admin-only, writeTargets อ่านเป้าหมายใน CTE ด้วย

commit ที่เกี่ยวข้อง

  • 239b0997 — write_policy + allow-list + ย้ายการเขียนหน้าจอไป Go (2026-10-21)
  • 923fae86 — เปลี่ยนเลข ADR เป็น 0011 (0006 ซ้ำ); checkout POS ใช้ลูกค้าเริ่มต้นของ posconfig เพราะ CASH ไม่ใช่ลูกค้า
  • 3ef39d32 — ถอด erp_user_datalist_config ออกจาก allow-list
  • 04903202 — ถอด barcode SQL fallback ของ Next (2026-10-22)
  • a47413ec — กฎเดียวกันที่ /blob/insert (2026-10-24)

โค้ดปัจจุบันที่ป้องกัน

  • backend/internal/domain/query/write_policy.go:24 moneyTablePattern, :64 ValidateClientInsert, :77 ValidateClientPolicy
  • backend/internal/domain/query/write_policy_legacy.go:9 รายการตารางที่ level ต่ำเขียนได้, :42, :55 isLegacyClientWritable; query/query.go:63 protectedSystemTablePattern
  • application/query/service.go:105,146 เรียก policy เมื่อ ClientRequest; adapter/inbound/http/query/handler.go:245,299 ตั้งค่า true; blob/handler.go:98
  • เทสต์: write_policy_test.go:8,51,65; backend/test/blob_integration_test.go:151
  • ข้อสังเกตจากการอ่านโค้ด (ไม่ใช่ประวัติ): router.go:85-88 สร้าง queryWriteGuard เฉพาะเมื่อมี Rights ถ้าไม่มีจะไม่ตรวจสิทธิ์ Add/Edit/Delete (นโยบายตารางยังทำงาน); HANDOFF §8 ระบุว่ายังมี master เขียนผ่าน query/exec

บทเรียน / กฎที่ได้

  1. ทุกประตูเขียน (exec, execBatch, blob insert, fallback ของ Next) ต้องผ่านฟังก์ชันตรวจเดียว
  2. อ่านเป้าหมายด้วย regex ไม่ได้ = ปฏิเสธ แล้วเพิ่มเทสต์ทุกรูปแบบที่หลุด
  3. ข้อมูลที่ผู้ใช้เก็บแล้วไปรันที่อื่น (SQL ใน datalist config) ห้ามอยู่ใน allow-list
  4. ย้ายหน้าจอไป Go ทุกครั้ง ถอดตารางจาก allow-list และเพิ่มเคสใน TestValidateClientPolicy
  5. fallback ฝั่ง proxy ที่เขียนข้อมูลเองคือช่องโหว่ ให้ error ตรง ๆ

ดูเพิ่ม

ADR-0011 ล็อก SQL เขียนจาก browser: ตารางเงิน/สต๊อก/GL ผ่าน /api/process เท่านั้น · ADR-0006 /query/exec รับเฉพาะ DML + ตารางระบบต้อง level>=9 (ถูกแทนด้วย ADR-0011) · ADR-0010 /blob/insert เก็บรูปเป็น bytea (ล้าสมัย: โค้ดใช้ MinIO แล้ว) · บทเรียนระบบสิทธิ์เมนู (Permission & Menu Rights) · บทเรียนธุรกรรมอะตอมมิกและ error ที่ถูกกลืน (Atomic Transactions & Swallowed Errors) · เช็กลิสต์ความปลอดภัย API: ตัวตน สิทธิ์ และช่องโหว่เว็บ (Secure API Checklist) · history-overview

ที่มาในโค้ด

  • git:239b0997, git:923fae86, git:3ef39d32, git:04903202, git:a47413ec
  • backend/docs/adr/0011-query-exec-write-lockdown.md
  • backend/internal/domain/query/write_policy.go:24, :64, :77
  • backend/internal/adapter/inbound/http/router.go:85-88