Korat
เขียนให้คนอ่านละเอียด

บัญชี สิทธิ์เข้าถึง และจุดที่เรายังทำไม่ได้

ความปลอดภัยข้อมูลของ Korat หน้านี้เขียนแบบที่จะอธิบายให้วิศวกรอีกคนฟัง: อะไรปกป้องบัญชีของคุณ อะไรปกป้องแถวข้อมูลของคุณในฐานข้อมูล และ — ที่ท้ายหน้า ด้วยขนาดตัวอักษรเท่ากัน — รายการสิ่งที่ Korat ไม่ได้ทำและไม่ควรถูกยกความดีความชอบให้

มีสองความคิดที่แบกน้ำหนักเกือบทั้งหมด บัญชีของคุณคือ session ของระบบ auth จริง ไม่ใช่แถวข้อมูลที่เราเอารหัสผ่านไปเทียบเอง และ ฐานข้อมูลเป็นคนตัดสินว่าคุณอ่านอะไรได้ ไม่ใช่แอป — ดังนั้นแอปที่ถูกดัดแปลง หรือคนที่ยิง API ตรง ๆ ด้วย public key จะได้คำตอบเหมือนกับที่แอปได้เป๊ะ ๆ

ความปลอดภัยข้อมูลของบัญชี bcrypt และ session จริง

ตัวตนอยู่บน Supabase Auth รหัสผ่านถูก hash ด้วย bcrypt ฝั่งเซิร์ฟเวอร์ แอปไม่เคยเห็น hash ของรหัสผ่าน และไม่มีการเทียบรหัสผ่านที่เขียนเองอยู่ในเส้นทางล็อกอินเลย การเข้าสู่ระบบจะได้ JWT ที่ผูกกับคุณ และคอลัมน์บนแถวโปรไฟล์ (auth_uid) คือจุดเชื่อมเดียวระหว่าง auth user กับโปรไฟล์ — ซึ่งเป็นเหตุผลที่ทำให้ทุก policy ด้านล่างเขียนออกมาได้

คุณเข้าสู่ระบบด้วยอีเมลกับรหัสผ่านก็ได้ หรือด้วย Google ผ่าน Credential Manager ของ Android ซึ่งส่ง Google ID token ตัวจริงให้ Supabase แลกเปลี่ยน การออกจากระบบทุกอุปกรณ์จะฆ่า session ทั้งหมดพร้อมกัน

ยังมีคอลัมน์ PIN ตกค้างจากยุคก่อนใช้ Supabase Auth อยู่ ไม่มีอะไรอ่านมันแล้ว บัญชีใหม่ถูกสร้างโดยปล่อยว่าง และมันเข้าคิวรอลบอยู่ ที่พูดถึงตรงนี้เพราะมันมีอยู่จริงและคุณจะเจอมันเข้าสักวัน

Passkey: ตรวจสอบบนเซิร์ฟเวอร์ พร้อมตัวกันการเล่นซ้ำ

Korat รองรับ passkey — คู่กุญแจที่สร้างอยู่ในฮาร์ดแวร์ปลอดภัยของเครื่องคุณ ปลดล็อกด้วยลายนิ้วมือหรือใบหน้า และไม่เคยออกจากเครื่อง relying party คือ koratland.com ผูกกับแอป Android ผ่าน assetlinks.json และการตรวจสอบรันอยู่ใน Cloudflare Worker ที่เขียนเอง ไม่ใช่เรียกไลบรารี:

  • challenge ใช้ได้ครั้งเดียว อายุห้านาที และแถวจะถูกลบทิ้งทันทีที่ถูกใช้
  • rpIdHash ใน authenticator data ต้องเท่ากับ SHA-256 ของ koratland.com
  • ต้องมีการตั้ง flag ว่าผู้ใช้อยู่ตรงหน้าเครื่องจริง (user-present)
  • ลายเซ็นถูกตรวจด้วย WebCrypto — ECDSA P-256 หรือ RS256
  • ตัวนับลายเซ็นต้องเพิ่มขึ้นเสมอ; ค่าที่ซ้ำเดิมหรือถอยหลังจะถูกปฏิเสธว่าเป็นการเล่นซ้ำ

การลงทะเบียน passkey ต้องมี session อยู่ก่อน และดึงตัวตนของผู้เรียกจาก bearer token ไม่ใช่จาก user id ที่ส่งมาใน request body — ไม่อย่างนั้นใครก็ลงทะเบียน passkey เข้าบัญชีคนอื่นได้ แถวข้อมูล credential ที่เก็บไว้ไม่มี UPDATE policy เลยสักข้อ เจ้าของเองก็เขียนทับ public key หรือตัวนับลายเซ็นของตัวเองไม่ได้ ส่วนการเปลี่ยนชื่อ passkey ผ่านฟังก์ชันที่แตะได้แค่ป้ายชื่อเท่านั้น

มีอย่างหนึ่งที่ตั้งใจไม่ทำ: ไม่ตรวจ attestation Korat ไม่เช็กว่าผู้ผลิตรายไหนทำ authenticator ตัวนั้น ฉะนั้นอย่าอ่านอะไรในนี้เป็นการรับรองระดับฮาร์ดแวร์

สถานะตามจริง การ*ลงทะเบียน* passkey ทดสอบแล้วว่าใช้งานได้ ส่วนการ*ล็อกอิน*ด้วย passkey ยังไม่เคยรันจบครบเส้นทาง และเว็บแอปยังไม่รองรับ passkey เลย — มันเป็นฟีเจอร์ฝั่ง Android ที่กำลังทยอยปล่อย เส้นทางที่ใช้งานจริงทุกวันคืออีเมลกับรหัสผ่าน และการเข้าสู่ระบบด้วย Google

Row-level security: ฐานข้อมูลเป็นคนตอบ ไม่ใช่แอป

RLS เปิดอยู่ทั่วทั้งฐานข้อมูล policy ทุกข้อเขียนด้วยฟังก์ชันสี่ตัว และเกือบทุกกฎใน Korat คือหนึ่งในสี่นี้:

app_uid()
id โปรไฟล์ของผู้เรียก หรือ null เมื่อไม่มีใครล็อกอิน เป็นฐานของทุก policy แบบ “แถวของตัวเองเท่านั้น”
is_member_of(business_id)
จริงสำหรับเจ้าของ หรือใครก็ตามที่มีแถวสมาชิกที่ยัง active อยู่กับธุรกิจนั้น
has_capability(business_id, capability)
ขยายชุดบทบาทสำเร็จรูป — เจ้าของ ผู้จัดการ พนักงานเสิร์ฟ ครัว — ฝั่งเซิร์ฟเวอร์ ฐานข้อมูลจึงไม่เคยเชื่อว่าไคลเอนต์เข้าใจว่าบทบาทหนึ่งแปลว่าอะไร
can_review(business_id)
อ่านจาก view ที่ตัดสินสิทธิ์รีวิว ทำให้ “เฉพาะลูกค้าจริงเท่านั้นที่รีวิวได้” เป็นคุณสมบัติของฐานข้อมูล ไม่ใช่คำสัญญาของแอป

มีสามกฎที่เรียนรู้มาด้วยราคาแพงพอจะเขียนไว้ policy ที่เขียนจากมุมของร้านอย่างเดียวจะกันลูกค้าออกไป — เซสชันโต๊ะ การเรียกพนักงาน และรายการเงิน ล้วนมีผู้อ่านที่ชอบธรรมสองฝ่าย จึงต้องเขียนว่า user_id = app_uid() or is_member_of(business_id) ตารางลูกต้องอ่านได้เท่ากับตารางแม่พอดี คอมเมนต์และไลก์จึงมอบหมายการตัดสินให้โพสต์ แทนที่จะถือความคิดของตัวเองว่าใครอ่านได้ และ ฟังก์ชัน definer รันในสิทธิ์ของเจ้าของ มันจึงต้องตรวจสิทธิ์ด้วยตัวเอง

ความปลอดภัยข้อมูล สิ่งที่ RLS ทำไม่ได้ และอะไรมาแทน

RLS ตอบได้ว่า *ใครเป็นคนเรียก* แต่ไม่มีทางตอบได้ว่า *คำขอนี้ถือ token ที่ถูกต้องมาหรือเปล่า* — QR ของโต๊ะ ลิงก์แชร์ คำเชิญ ทุกเส้นทางใน Korat ที่ล็อกด้วย token จึงเป็นฟังก์ชัน SECURITY DEFINER ที่ตรวจ token เอง ส่วนตารางยังปิดสนิทไว้อย่างนั้น

การเข้าร่วมโต๊ะ คนที่กำลังจะสแกน QR ของโต๊ะไม่ใช่เจ้าของเซสชันและไม่ใช่พนักงาน เขาจึงอ่านตารางเซสชันโต๊ะไม่ได้เลย — แม้แต่จะเปิดดู token ก็ไม่ได้ join_session_by_token จับคู่ token ฝั่งเซิร์ฟเวอร์ ตั้งให้คนแรกที่สแกนเป็นเจ้าของโต๊ะ เพิ่มผู้ร่วมโต๊ะ และ คืนค่าว่างเปล่าเมื่อ token ผิด แทนที่จะคืนข้อความที่เอาไปใช้ไล่เดาต่อได้

ลิงก์แชร์ resolve_share คืนแค่ชนิดกับปลายทางของลิงก์ ไม่คืนอย่างอื่นเลย — ไม่บอกว่าใครแชร์ ไม่บอกจำนวนคลิก ส่วนการนับคลิกผ่าน record_share_click ใครก็ถูกนับได้ โดยไม่ต้องมีสิทธิ์ insert บนตารางสถิติ และตั้งใจไม่มี INSERT policy ให้ใครทั้งนั้น

เรื่องเงิน การสั่งซื้อรันบนเซิร์ฟเวอร์ทั้งกระบวนการ มันคิดราคาทุกบรรทัดเอง และอ่านค่าส่งจากแถวของธุรกิจนั้นเอง โดยไม่สนใจค่าที่ไคลเอนต์ส่งมา — เพราะค่าส่งที่ไคลเอนต์ตั้งได้ คือส่วนลดที่ไคลเอนต์แจกตัวเองได้

ช่องทางติดต่อ อุปกรณ์ และการล็อกอินที่ไม่ใช่คุณ

รหัสยืนยันสำหรับอีเมลสำรองถูกสร้างโดยฟังก์ชันที่เรียกได้เฉพาะ service role เก็บเฉพาะค่า SHA-256 ของรหัสเท่านั้น หมดอายุใน 10 นาที ให้ลองได้ 5 ครั้ง และจำกัดคำขอที่นาทีละครั้ง ส่วน Worker ที่ส่งอีเมลไม่เคยส่งรหัสกลับมาให้แอปเลย

คุณตั้งช่องทางติดต่อของตัวเองให้ “ยืนยันแล้ว” ไม่ได้ trigger ในฐานข้อมูลบังคับให้ flag ยืนยันเป็นเท็จทุกครั้งที่ไคลเอนต์เขียน และมีเพียงฟังก์ชัน definer ที่ตรวจรหัสจริง ๆ แล้วเท่านั้นที่ตั้งค่านี้ได้ ฟังดูระแวงเกินไปจนกว่าจะลองคิดตามให้จบ: ถ้าไม่มีข้อนี้ การเอาอีเมลตัวเองไปใส่ในบัญชีคนอื่นแล้วกด “ลืมรหัสผ่าน” คือการยึดบัญชีแบบสมบูรณ์ ด้วยเหตุผลเดียวกัน ช่องทางติดต่อต้องยืนยันก่อนถึงจะตั้งเป็นช่องทางหลักได้ — เพราะช่องทางหลักคือที่อยู่ที่ระบบส่งเมลไปหา

ทุกอุปกรณ์ที่ล็อกอินอยู่จะอยู่ในรายการในหน้าตั้งค่า และเพิกถอนได้ทีละเครื่อง ตารางอุปกรณ์ไม่มี policy สำหรับ insert, update หรือ delete เลยสักข้อ เพราะอุปกรณ์ที่เพิ่งถูกเพิกถอนยังถือ token ที่ใช้ได้อยู่ และถ้าไม่ปิดไว้มันจะลบสถานะเพิกถอนของตัวเองได้ การเพิกถอนอุปกรณ์จะลบ session ที่อยู่เบื้องหลังด้วย refresh token จึงตายไปพร้อมกัน

การล็อกอินจากอุปกรณ์ใหม่จะเขียนการแจ้งเตือนในแอป ในทรานแซกชันเดียวกับที่สร้างแถวอุปกรณ์ มันจึงหายไปไม่ได้ และส่งอีเมลแจ้งด้วย — เฉพาะช่องทางที่ยืนยันแล้วเท่านั้น เพราะที่อยู่ที่ยังไม่ยืนยันอาจเป็นของคนอื่น อีเมลนี้ยิงได้อย่างมากหนึ่งครั้งต่ออุปกรณ์ และเฉพาะอุปกรณ์ที่เพิ่งถูกสร้างในหนึ่งชั่วโมงที่ผ่านมา endpoint นี้จึงถูกดัดแปลงเป็นเครื่องมือถล่มเมลใส่เจ้าของบัญชีไม่ได้

ขีดจำกัดของการเพิกถอน พูดกันตรง ๆ access token ที่ออกไปแล้วมีอายุประมาณหนึ่งชั่วโมง และเรียกคืนกลางทางไม่ได้ อุปกรณ์ที่ถูกเพิกถอนแต่ออฟไลน์อยู่จะยังใช้งานได้จนกว่า token จะหมดอายุ นั่นคือธรรมชาติของ JWT ไม่ใช่บั๊ก และเป็นเหตุผลที่เรามีรายการอุปกรณ์ให้ดู แทนที่จะเคลมว่าการเพิกถอนมีผลทันที

สิ่งที่คุณสั่งได้เกี่ยวกับบัญชีของตัวเอง

การลบบัญชีทำเองได้จากหน้าตั้งค่า ไม่ต้องส่งอีเมลหาใคร มันเป็น*คำขอ*ที่ถูกบันทึกไว้และยกเลิกได้ ไม่ใช่ปุ่มที่กดพลาดตอนตีสองแล้วจบ สถานะของคำขอเปิดดูได้จากในแอป และช่องทาง privacy@koratland.com ยังใช้ได้อยู่สำหรับสิทธิ์อื่นตาม PDPA ทั้งขอเข้าถึง แก้ไข ขอโอนย้าย และถอนความยินยอม

การรายงานครอบคลุมทั้งคนและเนื้อหา ทั้งโปรไฟล์ โพสต์ คอมเมนต์ story ข้อความ และเพจธุรกิจ ฟังก์ชันที่รับรายงานเป็น SECURITY INVOKER ซึ่งเป็นข้อยกเว้นหนึ่งเดียวในระบบหลังบ้านทั้งหมด เพราะถ้ามันรันด้วยสิทธิ์ของเจ้าของฐานข้อมูล มันจะ*มองเห็น*โพสต์ที่ถูกซ่อนและโพสต์ส่วนตัว แล้วตอบต่างออกไปสำหรับของพวกนั้น ซึ่งเปลี่ยนปุ่มรายงานให้กลายเป็นเครื่องมือไล่ถามว่าเนื้อหาชิ้นนั้นมีอยู่ไหม เมื่อมันรันด้วยสิทธิ์ของผู้เรียก "ไม่มีโพสต์นี้" กับ "โพสต์นี้ไม่ใช่ของที่คุณเห็นได้" จึงยุบเป็นคำตอบเดียวกัน ซึ่งซื่อสัตย์ เพราะจากที่ผู้รายงานยืนอยู่ สองอย่างนั้นคือเรื่องเดียวกัน ส่วนการจำกัดจำนวนครั้งอยู่ในทริกเกอร์ ไม่ใช่ในแอป

ความปลอดภัยข้อมูล สิ่งที่ Korat ไม่ได้ทำ

รายการนี้คือเหตุผลที่ทั้งหน้าข้างบนควรค่าแก่การอ่าน ทุกข้อในนี้เป็นความจริง ณ วันนี้

  • ไม่มีอะไรถูกเข้ารหัสแบบ end-to-end ข้อความ ไฟล์ และการโทร ถูกป้องกันระหว่างทางด้วย TLS และตอนพักอยู่ด้วย RLS เซิร์ฟเวอร์อ่านมันได้ Korat ไม่มีกุญแจฝั่งอุปกรณ์และไม่มีระบบจัดการกุญแจ อย่าเลือกใช้มันกับอะไรที่ต้องการสิ่งเหล่านั้น
  • ไม่มีการเข้ารหัสระดับแอปพลิเคชันตอนพักข้อมูล นอกเหนือจากที่ฐานข้อมูลแบบ managed ให้มาเอง
  • ไม่เคยมีการตรวจเอกสารยืนยันตัวตนสักใบ ไม่มี eKYC ไม่มีที่ไหนใน Korat เขียนว่า “ยืนยันตัวตนแล้ว” และตราความน่าเชื่อถือบอกแค่ว่ามีหลักฐานอะไรอยู่บ้างแทน
  • ยืนยันเบอร์โทรไม่ได้ — ยังไม่ได้ต่อกับผู้ให้บริการ SMS และ endpoint ก็ตอบไปตรง ๆ แทนที่จะแกล้งทำเป็นว่าได้
  • ไฟล์สื่อที่อัปโหลดอ่านได้สาธารณะผ่าน URL รูปโปรไฟล์ รูปในโพสต์ และรูปในแชท อยู่ใน bucket สาธารณะ การเขียนต้องมี session ก็จริง แต่ URL ที่หลุดออกไปคือไฟล์ที่ดึงได้ ยังไม่มี signed URL
  • การตรวจสิทธิ์ในคอนโซลธุรกิจตอนนี้รันอยู่บนเครื่อง โดยมีเมทริกซ์สิทธิ์ชุดเดียวกันอยู่ฝั่งเซิร์ฟเวอร์ในชื่อ has_capability() ให้ถือว่าสิทธิ์ในคอนโซลเป็นการควบคุมเชิงองค์กร ไม่ใช่เส้นแบ่งความปลอดภัย จนกว่าการย้ายจะเสร็จ
  • การโทรไม่มี TURN server และไม่ได้เข้ารหัสแบบ end-to-end การโทรข้ามเครือข่ายบางกรณีจะต่อไม่ติด
  • ไม่มีการสำรองฐานข้อมูลบนแพ็กเกจปัจจุบัน และไม่มีชุดทดสอบอัตโนมัติ
  • เว็บแอปยังไม่มีการแจ้งเตือนแบบพุช การแจ้งเตือนแบบพุชเป็นเรื่องของแอป Android เท่านั้น ส่วนบนเว็บคุณจะเห็นการแจ้งเตือนตอนเปิดแท็บ

ถ้าข้อไหนเปลี่ยน หน้านี้จะเปลี่ยนตาม เขียนย่อหน้าเรื่องการเข้ารหัสระดับธนาคารแล้วเดินจากไปนั้นง่ายกว่ามาก แต่แบบนี้มีประโยชน์กว่า

คำถามที่พบบ่อย

ข้อความใน Korat เข้ารหัสแบบ end-to-end ไหม

ไม่ ข้อความเดินทางผ่าน TLS และถูกป้องกันในฐานข้อมูลด้วย policy ของ RLS ที่ยอมให้เฉพาะคู่สนทนาสองคนอ่านได้ — แต่เซิร์ฟเวอร์อ่านได้ ถ้าคุณต้องการการเข้ารหัสแบบ end-to-end Korat ไม่ใช่เครื่องมือที่ถูกต้องสำหรับบทสนทนานั้น

ถ้ามีคนขโมยรหัสผ่านของฉันไปจะเกิดอะไรขึ้น

เขาจะไปสะกิดการแจ้งเตือนอุปกรณ์ใหม่ ทั้งในแอปและทางอีเมลไปยังช่องทางติดต่อที่คุณยืนยันแล้ว ตั้งแต่ครั้งแรกที่เขาล็อกอินจากเครื่องที่คุณไม่เคยใช้ คุณเพิกถอนอุปกรณ์นั้นได้จากหน้าตั้งค่า ซึ่งจะลบ session ของมันทิ้ง แล้วเปลี่ยนรหัสผ่าน การเพิ่ม passkey จะตัดรหัสผ่านออกจากสมการการโจมตีไปเลยบนอุปกรณ์นั้น

ร้านเห็นโปรไฟล์ฉันได้ไหมเพราะฉันสั่งของจากเขา

ร้านเห็นเท่าที่จำเป็นต่อการให้บริการออเดอร์นั้น — ชื่อคุณบนโต๊ะ รายการที่สั่ง และถ้าเป็นเดลิเวอรีก็เห็นที่อยู่ที่คุณกรอก ส่วนฟิลด์ในโปรไฟล์แต่ละช่องมีค่าความเป็นส่วนตัวของตัวเอง สาธารณะ / เพื่อน / ส่วนตัว และถูกบังคับใช้ตอนที่คนอื่นเปิดดูโปรไฟล์ ระบบสะสมแต้มและรายชื่อลูกค้าสร้างจากบัญชีการขาย ไม่ใช่จากโปรไฟล์ของคุณ

Korat สอดคล้องกับ PDPA หรือไม่

Korat ขอความยินยอมตาม PDPA ก่อนสร้างบัญชี ผ่านหน้าจอที่คุณต้องเลื่อนอ่านจนสุด และมีทางปฏิเสธจริง ๆ มันระบุว่าเก็บอะไรและเก็บไปทำไม ไม่ขายข้อมูลส่วนบุคคล และรองรับสิทธิ์ในการเข้าถึง แก้ไข ลบ คัดค้าน ขอโอนย้าย และถอนความยินยอม โดยการลบบัญชีสั่งได้เองจากหน้าตั้งค่าในแอปและยกเลิกคำขอได้ ส่วนสิทธิ์ที่เหลือติดต่อผ่าน privacy@koratland.com การปฏิบัติตามกฎหมายเป็นหน้าที่ต่อเนื่อง ไม่ใช่ตราที่ติดแล้วจบ และช่องว่างที่ระบุไว้ข้างบนก็เป็นส่วนหนึ่งของคำตอบที่ซื่อสัตย์

จะรายงานปัญหาด้านความปลอดภัยได้อย่างไร

ส่งอีเมลพร้อมรายละเอียดมาที่ hello@koratland.com ไม่มีโครงการให้รางวัล แต่รายงานจริงจะได้คำตอบจริง

มีคำถามที่หน้านี้ยังไม่ตอบ?

เขียนมาที่ hello@koratland.com คำถามด้านความปลอดภัยจะได้คำตอบตรง ๆ รวมถึงคำตอบว่า “เรายังไม่ได้ทำ”