// docs / baas security / supabase rls scanner
Supabase RLS scanner: ค้นหาตารางที่ขาดหรือมี row-level security ที่เสียหาย
Row-level security (RLS) คือสิ่งเดียวที่ยืนอยู่ระหว่างข้อมูลของลูกค้าและอินเทอร์เน็ตเมื่อคุณปล่อยแอปที่ใช้ Supabase เครื่องมือ AI Coding สร้างโค้ดในรูปแบบ RLS ที่ compile ได้, deploy ได้ และทำให้ข้อมูลรั่วไหลโดยเงียบๆ — ตารางที่สร้างขึ้นโดยไม่ได้เปิด RLS, policy ที่อ่านได้แต่ไม่จำกัด, predicate ที่เปรียบเทียบคอลัมน์กับตัวมันเอง บทความนี้แสดงว่า Supabase RLS scanner สามารถพิสูจน์อะไรได้บ้างจากภายนอก, รูปแบบ RLS เสียหายสี่แบบที่ปรากฏในแอป vibe-coded และวิธีสแกน deployment ของคุณเองในเวลาไม่ถึงหนึ่งนาที
การสแกน RLS จากภายนอกสามารถพิสูจน์อะไรได้
การสแกน RLS แบบ passive รันกับ PostgREST endpoint ที่ Supabase เปิดที่ https://[project].supabase.co/rest/v1/ โดยใช้เพียงแค่ anon key ที่เผยแพร่ได้ — key เดียวกับที่เบราว์เซอร์ของคุณใช้ — และตรวจสอบ metadata รายการตาราง, การอ่านแบบ anonymous และการเขียนแบบ anonymous มันไม่เคย authenticate ในฐานะผู้ใช้และไม่เคยใช้สิทธิ์ service-role อะไรก็ตามที่มันทำได้ ผู้โจมตีที่ไม่ผ่านการ authenticate บนอินเทอร์เน็ตก็ทำได้
จากภายนอกฐานข้อมูล สแกนเนอร์สามารถยืนยันสิ่งต่อไปนี้ด้วยความมั่นใจสูง:
- RLS ปิดอยู่บนตาราง PostgREST คืน rows สำหรับ
SELECTแบบ anonymous เมื่อ RLS ปิดอยู่หรือเมื่อ policy อนุญาต ทั้งสองกรณีถือเป็น finding - role ที่เป็น anonymous สามารถ list ตารางได้
GET /rest/v1/ด้วย anon key คืน OpenAPI schema สำหรับทุกตารางที่ roleanonมีสิทธิ์ใดๆ อยู่ แอปที่สร้างจาก AI มักให้สิทธิ์USAGEบน schema และSELECTบนทุกตาราง ทำให้แผนผัง schema ทั้งหมดถูกเปิดเผยแม้ว่า RLS จะปฏิเสธการอ่านจริงก็ตาม - role ที่เป็น anonymous สามารถ insert ได้
POSTทดลองพร้อมการคาดเดารูปแบบคอลัมน์จะสำเร็จถ้า RLS ไม่มีINSERTpolicy ปฏิเสธ — แม้ว่าSELECTจะถูกล็อกไว้ก็ตาม - service-role key อยู่ใน bundle ของเบราว์เซอร์ ใกล้เคียงกับ RLS: ถ้าสแกนเนอร์พบ
SUPABASE_SERVICE_ROLE_KEYหรือ JWT ใดๆ ที่มีrole: service_roleใน JavaScript bundle, RLS ก็ไม่มีผล — ผู้ที่ถือ key นั้นข้ามทุก policy
การสแกนจากภายนอกพิสูจน์อะไรไม่ได้
เปิดเผยตรงๆ เกี่ยวกับข้อจำกัดของสแกนเนอร์ การสแกน RLS จากภายนอกไม่สามารถอ่านตาราง pg_policies ของคุณ, ไฟล์ migration หรือ predicate ที่แน่นอนของ policy ใดๆ ได้ มันอนุมานจากพฤติกรรม black-box ซึ่งหมายความว่าบางครั้งอาจรายงาน finding ที่กลายเป็นข้อมูลสาธารณะโดยตั้งใจ (ตาราง newsletter การตลาด, catalog สินค้าสาธารณะ) รายงาน FixVibe จะระบุสิ่งเหล่านี้ว่าเป็น ความมั่นใจระดับกลาง เมื่อสแกนเนอร์ไม่สามารถแยกเจตนาได้ — ดูชื่อตารางและตัดสินใจ
รูปแบบ RLS เสียหายสี่แบบที่เครื่องมือ AI สร้างขึ้น
เมื่อคุณใช้ Cursor, Claude Code, Lovable หรือ Bolt กับ Supabase รูปแบบ RLS เสียหายสี่แบบเดียวกันจะปรากฏในแอปนับพัน แต่ละแบบผ่าน type-check, compile ได้ และ deploy ได้:
แบบที่ 1: RLS ไม่เคยถูกเปิดใช้งาน
โหมดความล้มเหลวที่พบบ่อยที่สุด migration สร้างตารางแต่ developer (หรือเครื่องมือ AI) ลืม ALTER TABLE ... ENABLE ROW LEVEL SECURITY PostgREST ให้ตารางทั้งหมดแก่ใครก็ตามที่มี anon key อย่างสบายใจ แก้ไข: ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY; ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY; FORCE ไม่ใช่ทางเลือก — หากไม่มี เจ้าของตาราง (และ role ใดๆ ที่เป็นเจ้าของตาราง) จะข้าม RLS
ALTER TABLE public.[name] ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.[name] FORCE ROW LEVEL SECURITY;แบบที่ 2: เปิด RLS แล้วแต่ไม่มี policy
ความล้มเหลวที่แอบแฝงกว่า RLS เปิดอยู่แต่ไม่มี policy เขียนไว้ ค่าเริ่มต้นใน PostgreSQL คือ ปฏิเสธ ดังนั้นผู้ใช้ที่ผ่านการ authenticate จะไม่เห็นอะไรเลย — และ developer เพิ่ม USING (true) เพื่อให้แอปทำงาน ซึ่งอนุญาตให้ทุกคนอ่านทุกอย่าง แก้ไข: เขียน policy ที่จำกัดด้วย auth.uid(): CREATE POLICY "select_own" ON public.[name] FOR SELECT USING (auth.uid() = user_id); และ policy INSERT/UPDATE/DELETE ที่สอดคล้องกัน
CREATE POLICY "select_own"
ON public.[name]
FOR SELECT
USING (auth.uid() = user_id);แบบที่ 3: policy เปรียบเทียบคอลัมน์กับตัวมันเอง
A copy-paste artefact. The developer writes <code>USING (user_id = user_id)</code> — which is always true — instead of <code>USING (auth.uid() = user_id)</code>. Type-checks pass; the policy permits every row. <strong>Fix:</strong> always compare a column to a function call (<code>auth.uid()</code>, <code>auth.jwt()->>'org_id'</code>, etc.), never to itself or to a constant.
แบบที่ 4: policy บน SELECT แต่ไม่มีบน INSERT/UPDATE
developer ล็อกการอ่านแต่ลืมการเขียน RLS policy เป็นแบบต่อคำสั่ง FOR SELECT ปกป้องเฉพาะการอ่าน; client ที่เป็น anonymous ยังคงสามารถ INSERT ได้หากไม่มี policy ปฏิเสธ แก้ไข: เขียน policy ต่อคำสั่ง หรือใช้ FOR ALL พร้อมเงื่อนไข USING และ WITH CHECK ที่ชัดเจน
วิธีที่ FixVibe Supabase RLS scanner ทำงาน
Check baas.supabase-rls ทำงานในสามขั้นตอน แต่ละขั้นมีระดับความมั่นใจที่ชัดเจน:
- ขั้นที่ 1 — fingerprint สแกนเนอร์ crawl แอปที่ deploy แล้ว, parse JavaScript bundle และดึง URL ของ Supabase project และ anon key จากการตั้งค่า runtime ไม่มีการเดา DNS ไม่มี brute force — มันอ่านสิ่งที่เบราว์เซอร์อ่าน
- ขั้นที่ 2 — การค้นพบ schema
GET /rest/v1/เพียงครั้งเดียวด้วย anon key คืน OpenAPI schema สำหรับทุกตารางที่ anon role เห็นได้ สแกนเนอร์บันทึกชื่อตารางแต่ไม่อ่านข้อมูล row ในขั้นนี้ - ขั้นที่ 3 — probe การอ่านและเขียน สำหรับแต่ละตารางที่พบ สแกนเนอร์ส่ง
SELECTแบบ anonymous หนึ่งครั้งด้วยlimit=1หากคืน rows มา RLS เป็นแบบ permissive สแกนเนอร์หยุดที่นั่น — มันไม่ enumerate rows, ไม่ paginate, ไม่แก้ไขข้อมูล INSERT probe ถูกจำกัดให้อยู่หลังการยืนยันสิทธิ์ความเป็นเจ้าของโดเมนและการ opt-in อย่างชัดเจน; มันไม่เคยทำกับเป้าหมายที่ยังไม่ได้ยืนยัน
ทุก finding มาพร้อมกับ URL request ที่ชัดเจน, response status, รูปแบบ response (เฉพาะ header) และชื่อตาราง AI fix prompt ที่ด้านล่างของ finding เป็น SQL block ที่ copy-paste ได้ คุณรันใน Supabase SQL editor
จะทำอย่างไรเมื่อสแกนเนอร์พบบางสิ่ง
ทุก RLS finding คือเหตุฉุกเฉิน runtime PostgREST endpoint สาธารณะถูกสแกนโดยผู้โจมตีภายในไม่กี่นาที ลำดับการแก้ไขเป็นไปอย่างเป็นกลไก:
- Audit ทุกตาราง รัน
SELECT schemaname, tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public';ใน Supabase SQL editor row ใดๆ ที่มีrowsecurity = falseคือปัญหา - เปิด RLS บนทุกตารางสาธารณะ เป็นค่าเริ่มต้น
ENABLE ROW LEVEL SECURITYและFORCE ROW LEVEL SECURITYบนทุกตารางที่สร้าง — ทำให้เป็น template ของ migration - เขียน policy แยกคำสั่งทีละคำสั่ง อย่าใช้
FOR ALL USING (true)เขียน policy ที่ชัดเจนสำหรับ SELECT, INSERT, UPDATE, DELETE — แต่ละตัวจำกัดด้วยauth.uid()หรือคอลัมน์ org-id จากauth.jwt() - ตรวจสอบด้วยบัญชีที่สอง สมัครเป็นผู้ใช้คนละคน พยายามอ่านข้อมูลของผู้ใช้คนอื่นผ่าน REST API โดยตรง ถ้า response เป็น
200, policy เสียหาย - สแกนใหม่ หลังจากใช้การแก้ไข รัน FixVibe scan อีกครั้งกับ URL เดิม Finding
baas.supabase-rlsควรหายไป
-- Audit every table for missing RLS. Run in the Supabase SQL editor.
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY rowsecurity, tablename;เปรียบเทียบกับสแกนเนอร์อื่นๆ อย่างไร
เครื่องมือ DAST ทั่วไปส่วนใหญ่ (Burp Suite, OWASP ZAP, Nessus) ไม่รู้จัก PostgREST พวกมันจะ crawl แอปของคุณ, เพิกเฉยต่อเส้นทาง /rest/v1/ และรายงานเฉพาะหน้า HTML ที่พวกมันเข้าใจ Snyk และ Semgrep เป็นเครื่องมือวิเคราะห์แบบ static — พวกมันค้นหาไฟล์ migration ใน repo ของคุณที่ขาดการเรียก RLS แต่ไม่สามารถพิสูจน์ว่าฐานข้อมูลที่ deploy แล้วถูกตั้งค่าผิด FixVibe อยู่ในช่องว่างนี้: passive, รู้จัก BaaS, มุ่งเน้นที่สิ่งที่ผู้โจมตีที่ไม่ผ่านการ authenticate สามารถพิสูจน์จาก URL สาธารณะ
คำถามที่พบบ่อย
สแกนเนอร์จะอ่านหรือแก้ไขข้อมูลของฉันหรือไม่?
ไม่ การสแกนแบบ passive ส่ง SELECT ... limit=1 มากที่สุดหนึ่งครั้งต่อตารางที่พบ เพื่อยืนยันว่า RLS อนุญาตการอ่านแบบ anonymous หรือไม่ สแกนเนอร์บันทึกรูปแบบ response ไม่ใช่เนื้อหา row INSERT, UPDATE และ DELETE probe ถูกจำกัดให้อยู่หลังการยืนยันสิทธิ์ความเป็นเจ้าของโดเมนและไม่เคยทำกับเป้าหมายที่ยังไม่ได้ยืนยัน
ใช้งานได้หรือไม่ถ้าโปรเจกต์ Supabase ของฉันถูกพักหรืออยู่บนโดเมนกำหนดเอง?
โปรเจกต์ที่ถูกพักจะคืน 503 ในทุก request — สแกนเนอร์รายงานว่าโปรเจกต์เข้าถึงไม่ได้ โดเมนกำหนดเองทำงานได้ตราบใดที่แอปที่ deploy แล้วยังคงโหลด Supabase client SDK ในเบราว์เซอร์; สแกนเนอร์ดึง URL ของโปรเจกต์จาก bundle ไม่ว่ากรณีใด
ถ้า anon key ของฉันถูกหมุนหรือ publishable key เปลี่ยน?
สแกนใหม่ สแกนเนอร์ดึง key ใหม่จาก bundle ปัจจุบันทุกครั้งที่รัน การหมุน key จะทำให้รายงานก่อนหน้าเท่านั้นที่ใช้ไม่ได้ ไม่ใช่สถานะ policy ของฐานข้อมูล
สแกนเนอร์ตรวจสอบโมเดล publishable key ใหม่ของ Supabase (<code>sb_publishable_*</code>) หรือไม่?
ใช่ ตัวตรวจจับรู้จักทั้ง anon JWT แบบเก่าและ key sb_publishable_* ที่ใหม่กว่า และจัดการพวกมันเหมือนกัน — ทั้งสองตั้งใจให้เป็นสาธารณะ และทั้งสองทำให้ RLS เป็นแนวป้องกันเพียงอย่างเดียว
ขั้นตอนถัดไป
รัน FixVibe scan ฟรีกับ URL production ของคุณ — check baas.supabase-rls เปิดใช้ในทุกแพ็กเกจรวมถึงระดับฟรี สำหรับการอ่านเพิ่มเติมเกี่ยวกับสิ่งอื่นๆ ที่อาจรั่วออกมาจากโปรเจกต์ Supabase โปรดดู Supabase service role key ถูกเปิดเผยใน JavaScript และ Supabase storage bucket checklist ความปลอดภัย สำหรับมุมมองภาพรวมข้ามผู้ให้บริการ BaaS ทั้งหมด อ่าน BaaS misconfiguration scanner
