// docs / baas security / supabase storage
Supabase storage bucket checklist ความปลอดภัย: 22 ข้อ
Supabase Storage เป็น wrapper บาง ๆ รอบ bucket ที่เข้ากันได้กับ S3 บวกกับโมเดล Row-Level Security เหมือนกับฐานข้อมูล นั่นหมายความว่าข้อผิดพลาด RLS แบบเดียวกันที่ส่งผลกระทบต่อตารางก็ส่งผลกระทบต่อการเข้าถึงไฟล์ — และยังมีข้อผิดพลาดเฉพาะ storage อีกไม่กี่ข้อที่ปรากฏเมื่อเครื่องมือ AI Coding เชื่อมต่อการอัปโหลด checklist นี้คือ 22 ข้อใน 5 ส่วน: การตั้งค่า bucket, RLS policy, การตรวจสอบการอัปโหลด, signed URL และ operational hygiene แต่ละข้อตรวจสอบได้ในเวลาไม่ถึง 15 นาที
ทุกข้อด้านล่างมีความสำคัญ สำหรับกลไก RLS ที่อยู่เบื้องหลัง โปรดดู Supabase RLS scanner สำหรับ class การเปิดเผย key ที่อยู่ใกล้ storage โปรดดู Supabase service role key ถูกเปิดเผยใน JavaScript
การตั้งค่า bucket
เริ่มต้นด้วยค่าเริ่มต้นที่ถูกต้อง bucket ที่ตั้งค่าผิดทำให้ไฟล์รั่วไหลไม่ว่า RLS ของคุณจะถูกต้องหรือไม่
- ตั้งค่า bucket ทุกตัวเป็น private โดยค่าเริ่มต้น ใน Supabase Dashboard → Storage → Buckets ตั้งค่า toggle Public bucket เป็นปิดเว้นแต่คุณจะมีเหตุผลที่ชัดเจน (asset การตลาด, avatar สาธารณะที่ไม่มี PII) Public bucket ข้าม RLS สำหรับการอ่าน — ใครก็ตามที่มีชื่อ bucket สามารถ list และดาวน์โหลดได้
- ตั้งค่าจำกัดขนาดไฟล์เด็ดขาดในทุก bucket Dashboard → Bucket settings → File size limit 50 MB เป็นค่าเริ่มต้นที่สมเหตุสมผลสำหรับการอัปโหลดของผู้ใช้; เพิ่มอย่างจงใจสำหรับวิดีโอ / กรณีใช้งานไฟล์ขนาดใหญ่ หากไม่มีการจำกัด การอัปโหลดที่เป็นอันตรายเพียงครั้งเดียวสามารถทำให้พื้นที่ storage หรือ bandwidth รายเดือนของคุณหมด
- จำกัด MIME type ที่อนุญาตต่อ bucket รายการ MIME types ที่อนุญาต — allowlist ที่ชัดเจน ไม่ใช่ blocklist
image/jpeg,image/png,image/webpสำหรับ bucket ที่เก็บเฉพาะรูปภาพ ห้ามอนุญาตtext/html,application/javascriptหรือimage/svg+xmlใน bucket เนื้อหาของผู้ใช้ — พวกมัน execute ในเบราว์เซอร์เมื่อเสิร์ฟผ่าน signed URL - ใช้ bucket หนึ่งต่อประเภทเนื้อหาหนึ่ง ไม่ใช่ bucket ร่วมเดียว การตั้งค่าต่อ bucket (ขนาด, MIME type, RLS policy) คือระดับความละเอียดที่คุณมี bucket ของ
user-avatars, bucket ของdocument-uploadsและ bucket ของpublic-assetsล็อกได้ง่ายกว่า bucket ผสมเดียว - ตรวจสอบการตั้งค่า CORS หากอัปโหลดจาก frontend ถ้าผู้ใช้อัปโหลดจากเบราว์เซอร์โดยตรงไปที่ signed URL CORS ของ bucket ต้องระบุ origin production ของคุณ
*ยอมรับได้สำหรับ public bucket เท่านั้น — ห้ามใช้กับ bucket ที่มี PII ของผู้ใช้
RLS policy บน storage.objects
Supabase Storage เก็บ metadata ของไฟล์ในตาราง storage.objects RLS บนตารางนั้นควบคุมว่าใครสามารถอ่าน, อัปโหลด, อัปเดต หรือลบไฟล์ได้ หากไม่มี RLS ธง public/private ของ bucket คือการป้องกันเพียงอย่างเดียวของคุณ
- ยืนยันว่า RLS เปิดอยู่บน storage.objects
SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects';ต้องคืนtrueSupabase เปิดมันเป็นค่าเริ่มต้นบนโปรเจกต์ใหม่; ตรวจสอบว่ามันไม่ได้ถูกปิด - เขียน SELECT policy ที่จำกัดด้วย
auth.uid()สำหรับ private bucketCREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);ตามแบบแผน เก็บไฟล์ใต้[user-id]/[filename]และใช้storage.foldername()เพื่อดึงเจ้าของจาก path - เขียน INSERT policy ที่บังคับแบบแผน path เดียวกัน
CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);หากไม่มี WITH CHECK ผู้ใช้ที่ผ่านการ authenticate สามารถอัปโหลดเข้าไปในโฟลเดอร์ของผู้ใช้อื่นได้ - เพิ่ม UPDATE และ DELETE policy หากแอปของคุณรองรับการแก้ไขหรือลบไฟล์ แต่ละคำสั่งต้องมี policy ของตัวเอง การข้าม DELETE หมายความว่าผู้ใช้ที่ผ่านการ authenticate ไม่สามารถลบไฟล์ของตัวเองได้; การข้าม UPDATE หมายความว่าการเขียนทับไฟล์ล้มเหลวอย่างเงียบ ๆ
- ทดสอบการเข้าถึงข้ามผู้ใช้ใน session เบราว์เซอร์สอง session เข้าสู่ระบบในฐานะ User A, อัปโหลดไฟล์, copy path เข้าสู่ระบบในฐานะ User B ในเบราว์เซอร์อื่น พยายามดึงไฟล์ผ่าน REST API response ต้องเป็น
403หรือ404, ไม่เคยเป็น200
-- Confirm RLS on storage.objects
SELECT rowsecurity
FROM pg_tables
WHERE schemaname = 'storage' AND tablename = 'objects';
-- SELECT policy: scope reads to the owning user's folder.
CREATE POLICY "users_read_own_files"
ON storage.objects
FOR SELECT
USING (auth.uid()::text = (storage.foldername(name))[1]);
-- INSERT policy: enforce the [user-id]/[filename] path convention.
CREATE POLICY "users_upload_own"
ON storage.objects
FOR INSERT
WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);การตรวจสอบการอัปโหลด
ตรวจสอบการอัปโหลดทุกครั้งฝั่งเซิร์ฟเวอร์ แม้ว่า bucket จะมีข้อจำกัด MIME และขนาด เครื่องมือ AI Coding สร้างการตรวจสอบเฉพาะฝั่ง client โดยค่าเริ่มต้น; นั่นไม่ปกป้องอะไรเลย
- ตรวจสอบ MIME type ใหม่ฝั่งเซิร์ฟเวอร์จากไบต์จริงของไฟล์ ไม่ใช่จาก header
Content-Typeใช้ library อย่างfile-type(Node) หรือ magic-byte sniffing ผู้โจมตีสามารถอ้างContent-Type: image/jpegบนไฟล์ที่จริงๆ แล้วเป็น polyglot HTML / JavaScript payload - ลบ EXIF metadata จากภาพที่อัปโหลด EXIF สามารถมีพิกัด GPS, หมายเลข serial ของอุปกรณ์ และ timestamp ใช้
sharpพร้อม.withMetadata(false)หรือexif-parserเพื่อลบก่อนการจัดเก็บ - ปฏิเสธ SVG ที่มี
scripttag หรือonloadhandler SVG เป็น XML — และแอปที่สร้างจาก AI หลายตัวอนุญาตการอัปโหลด SVG ในฐานะ "ก็แค่รูปภาพ" ใช้DOMPurifyฝั่งเซิร์ฟเวอร์หรือปฏิเสธการอัปโหลด SVG ทั้งหมด - ใช้ชื่อไฟล์ที่กำหนดได้และคาดเดาไม่ได้ อย่าเก็บชื่อไฟล์ดั้งเดิม ใช้ UUID หรือ hash ของเนื้อหาไฟล์ ชื่อไฟล์ดั้งเดิมรั่วไหล ("
passport_scan_2024_01_15.jpg") และชื่อที่คาดเดาได้ทำให้ enumerate ได้
Signed URL
Signed URL คือวิธีที่ client เข้าถึง private bucket ระยะเวลาหมดอายุ, ขอบเขต bucket และสิ่งที่ log ล้วนสำคัญ
- ตั้งค่าระยะเวลาหมดอายุ signed-URL เริ่มต้นเป็น 1 ชั่วโมงหรือน้อยกว่า Supabase JS SDK
createSignedUrl(path, expiresIn)รับเป็นวินาที ห้ามใช้ค่าเช่น31536000(หนึ่งปี) — URL กลายเป็นลิงก์กึ่งสาธารณะถาวร - ห้ามเก็บ signed URL ในฐานข้อมูล สร้างใหม่ฝั่งเซิร์ฟเวอร์ในทุก request signed URL ที่เก็บไว้ซึ่งมีอายุ 1 ปีและรั่วไหลผ่าน database dump จะให้สิทธิ์การเข้าถึงระยะยาว
- Log การสร้าง signed URL ไม่ใช่แค่การอัปโหลดไฟล์ หากคุณสงสัยว่ามีการบุกรุกในภายหลัง คุณต้องรู้ว่าใครสร้าง URL ไหนเมื่อไหร่ Log
auth.uid()+ bucket + object path + timestamp - ใช้ option
downloadAsเมื่อเสิร์ฟไฟล์ที่ผู้ใช้อัปโหลดcreateSignedUrl(path, expiresIn, { download: '.jpg' })บังคับ headerContent-Disposition: attachmentดังนั้นไฟล์ดาวน์โหลดแทนที่จะ render — ปิด class การ execute HTML / SVG / HTML-in-PDF
Operational hygiene
การตั้งค่า storage เลื่อนไหลตามกาลเวลา สี่ข้อนี้ทำให้พื้นผิวคงความตึง
- Audit bucket ทุกไตรมาส Dashboard → Storage → Buckets ยืนยันสถานะ public/private และรายการ MIME-type ตรงกับที่แอปคาดหวัง bucket ที่สร้าง "ชั่วคราว" กลายเป็นถาวรหากไม่มีใครลบ
- ตรวจสอบการดำเนินการ list แบบ anonymous Storage log (Dashboard → Logs → Storage) บันทึก request
LISTการเพิ่มขึ้นของ anonymous list request กับ private bucket หมายความว่ามีคนกำลังตรวจสอบจากภายนอก - ตั้งนโยบายการเก็บรักษาสำหรับการอัปโหลดชั่วคราว Temp bucket (image preview, draft upload) ควรลบอัตโนมัติหลังจาก 24-72 ชั่วโมงผ่านฟังก์ชันที่กำหนดเวลา การเก็บรักษาแบบไม่จำกัดเป็นภาระภายใต้ภาระผูกพันการลดข้อมูลของ GDPR / CCPA
- รัน FixVibe scan รายเดือน check
baas.supabase-storage-publicตรวจสอบ bucket ที่ตอบสนองต่อ anonymousGET+LISTbucket ใหม่ถูกเพิ่ม; bucket เก่าเปลี่ยน visibility — เฉพาะการสแกนต่อเนื่องเท่านั้นที่จับการเลื่อนไหลได้
ขั้นตอนถัดไป
รัน FixVibe scan กับ URL production ของคุณ — รายการ storage แบบ anonymous ปรากฏใต้ baas.supabase-storage-public จับคู่ checklist นี้กับ Supabase RLS scanner สำหรับชั้นตารางและ Supabase service role key ถูกเปิดเผยใน JavaScript สำหรับ adjacency การเปิดเผย key สำหรับการตั้งค่า storage ผิดข้าม BaaS provider อื่นๆ โปรดดู BaaS misconfiguration scanner
