← กลับไปหน้าบทความ
คู่มือ

ไฟล์ที่อัปโหลดขึ้น Supabase Storage ไม่ได้ปลอดภัยอัตโนมัติ ต้องผูก RLS เอง

ทีมบรรณาธิการ linli12 ส.ค. 04:52อัปเดต 12 ส.ค. 04:52อ่าน 3 นาที
ไฟล์ที่อัปโหลดขึ้น Supabase Storage ไม่ได้ปลอดภัยอัตโนมัติ ต้องผูก RLS เอง
บทความนี้จัดทำเพื่อความรู้ ชื่อธุรกิจ ตัวเลข หรือบทสนทนาที่ไม่มีแหล่งอ้างอิงกำกับเป็นกรณีตัวอย่างเพื่ออธิบายแนวคิด ดูรายละเอียดที่ นโยบายกองบรรณาธิการ

สรุปสั้น ๆ

Supabase Storage คือระบบเก็บไฟล์ที่ผูกกับ Postgres โดยจัดเก็บ metadata ของไฟล์ไว้ในตาราง storage.objects ทำให้สามารถเขียน RLS policy ควบคุมว่าใครอัปโหลด อ่าน หรือลบไฟล์ได้บ้าง เหมือนกับควบคุมสิทธิ์ตารางข้อมูลทั่วไป bucket ที่ตั้งเป็น public จะข้ามการตรวจสอบสิทธิ์นี้ไปเลย จึงต้องระวังเป็นพิเศษว่า bucket ไหนควรเป็น public จริง ๆ

ข้อเข้าใจผิดที่พบบ่อยที่สุดเกี่ยวกับ Supabase Storage คือคิดว่าแค่อัปโหลดไฟล์ขึ้นไปแล้วระบบจะจัดการเรื่องความปลอดภัยให้เองโดยอัตโนมัติ เหมือนกับที่หลายคนคุ้นเคยกับบริการเก็บไฟล์ทั่วไปที่ตั้งค่าครั้งเดียวจบ แต่ Supabase Storage ออกแบบมาให้ทำงานร่วมกับ Row Level Security แบบเดียวกับตารางข้อมูล ซึ่งหมายความว่าถ้าไม่ตั้ง policy ให้ถูกต้อง ไฟล์อาจเปิดกว้างเกินไปหรือปิดจนใช้งานไม่ได้เลย

จุดที่ทำให้ Storage ของ Supabase น่าสนใจคือมันไม่ได้เป็นแค่ที่เก็บไฟล์แยกต่างหาก แต่ metadata ของทุกไฟล์ถูกเก็บไว้ในตาราง storage.objects ภายใน Postgres เดียวกับข้อมูลอื่นในโปรเจกต์ ทำให้สามารถเขียน policy ควบคุมสิทธิ์ไฟล์ได้ด้วยภาษา SQL แบบเดียวกับที่ใช้ควบคุมตารางทั่วไป

บทความนี้จะอธิบายว่า Storage ทำงานยังไงในเชิงโครงสร้าง แบ่ง bucket แบบ public กับ private ต่างกันยังไง เขียน policy ควบคุมสิทธิ์ไฟล์แบบไหนที่ใช้งานจริงได้ และข้อผิดพลาดที่พบบ่อยเมื่อทีมเริ่มใช้งาน

Supabase Storage เก็บไฟล์ยังไง เชื่อมกับ Postgres ตรงไหน

ไฟล์ที่อัปโหลดเข้า Supabase Storage จริง ๆ แล้วถูกเก็บไว้บนระบบเก็บข้อมูลแบบ object storage ที่ออกแบบมาสำหรับไฟล์ขนาดใหญ่โดยเฉพาะ แต่ข้อมูลที่บอกว่าไฟล์นั้นชื่ออะไร อยู่ใน bucket ไหน ใครเป็นเจ้าของ และอัปโหลดเมื่อไหร่ ถูกเก็บไว้ในตาราง storage.objects ภายใน Postgres เดียวกับข้อมูลอื่นของโปรเจกต์

การแยกแบบนี้ทำให้ระบบตรวจสอบสิทธิ์ทำงานผ่านกลไกเดียวกับ RLS ที่ใช้กับตารางทั่วไป เพราะทุกครั้งที่มีคำขออัปโหลด อ่าน หรือลบไฟล์ ระบบจะตรวจสอบ policy บนตาราง storage.objects ก่อนว่าอนุญาตให้ทำ operation นั้นกับไฟล์นั้นหรือไม่ โดยอ้างอิงผู้ใช้ที่ยืนยันตัวตนแล้วผ่านระบบเดียวกับที่อธิบายไว้ใน Supabase Auth คืออะไร

ไฟล์แต่ละไฟล์ถูกจัดกลุ่มไว้ใน bucket ซึ่งเปรียบเหมือนโฟลเดอร์ระดับบนสุด แต่ละ bucket ตั้งค่าเป็น public หรือ private แยกกันได้ และสามารถเขียน policy แยกตาม bucket ได้ด้วยเช่นกัน ไม่จำเป็นต้องใช้กฎเดียวกันทั้งโปรเจกต์ การอัปโหลดไฟล์จากฝั่งเซิร์ฟเวอร์ เช่นผ่าน Supabase Edge Functions ก็ทำได้เช่นกันโดยใช้ service_role key

bucket แบบ public กับ private ต่างกันยังไง เลือกใช้แบบไหน

bucket แบบ public คือไฟล์ที่ใครก็เข้าถึง URL ตรงได้เลยโดยไม่ต้องผ่านการตรวจสอบสิทธิ์ เหมาะกับไฟล์ที่ตั้งใจให้เปิดเผยอยู่แล้ว เช่นรูปโปรไฟล์สาธารณะ โลโก้สินค้า หรือไฟล์ static ที่ใช้แสดงผลบนหน้าเว็บทั่วไป การเข้าถึงแบบนี้เร็วและไม่มีค่าใช้จ่ายในการตรวจสอบสิทธิ์ทุกครั้ง

bucket แบบ private ต้องผ่านการตรวจสอบสิทธิ์ก่อนถึงจะเข้าถึงไฟล์ได้ ไม่ว่าจะเป็นการตรวจผ่าน policy โดยตรงหรือผ่าน signed URL ที่มีอายุจำกัด เหมาะกับไฟล์ที่มีข้อมูลส่วนตัว เช่นเอกสารยืนยันตัวตน ใบเสร็จ หรือไฟล์แนบในระบบที่จำกัดสิทธิ์ผู้เห็น

ข้อผิดพลาดที่พบได้บ่อยคือตั้ง bucket เป็น public เพราะทำให้ทดสอบง่ายกว่าตอนพัฒนา แล้วลืมเปลี่ยนกลับเป็น private ก่อนขึ้น production ทั้งที่ไฟล์ในนั้นมีข้อมูลที่ไม่ควรเปิดเผย ควรกำหนดตั้งแต่ต้นว่า bucket ไหนต้อง private แล้วยึดตามนั้นตลอดวงจรการพัฒนา ไม่ใช่ปรับตามความสะดวกชั่วคราว

ลักษณะPublic bucketPrivate bucket
เข้าถึงไฟล์โดยตรงผ่าน URLได้ทันทีไม่ต้องตรวจสอบสิทธิ์ต้องผ่าน policy หรือ signed URL
เหมาะกับไฟล์ประเภทไหนรูปโปรไฟล์สาธารณะ โลโก้ ไฟล์ staticเอกสารส่วนตัว ใบเสร็จ ไฟล์แนบภายใน
ความเร็วในการเข้าถึงเร็วกว่า ไม่มีขั้นตอนตรวจสอบมีขั้นตอนตรวจสอบเพิ่มเล็กน้อย

เขียน RLS policy ควบคุมสิทธิ์ไฟล์ยังไง

เพราะ metadata ของไฟล์อยู่ในตาราง storage.objects การเขียน policy ควบคุมสิทธิ์ก็ใช้หลักการเดียวกับที่อธิบายไว้ใน Supabase RLS คืออะไร เพียงแต่เงื่อนไขมักอ้างอิงคอลัมน์อย่าง bucket_id, name, หรือ owner แทนคอลัมน์ธุรกิจทั่วไป

  1. เปิด RLS บนตาราง storage.objects (ปกติเปิดเป็นค่าเริ่มต้นอยู่แล้วในโปรเจกต์ใหม่)
  2. เขียน policy สำหรับ SELECT ที่อนุญาตให้ผู้ใช้อ่านเฉพาะไฟล์ที่ตัวเองเป็นเจ้าของ หรือไฟล์ที่อยู่ใน path ที่เกี่ยวข้องกับตัวเอง เช่นตรวจสอบว่า owner ตรงกับ auth.uid()
  3. เขียน policy สำหรับ INSERT ที่ตรวจสอบว่าไฟล์ที่กำลังอัปโหลดถูกวางไว้ใน path ที่ผู้ใช้มีสิทธิ์ เช่นบังคับให้ path ต้องขึ้นต้นด้วย user id ของตัวเองเสมอ ป้องกันไม่ให้ผู้ใช้อัปโหลดไฟล์ไปทับ path ของคนอื่น
  4. เขียน policy สำหรับ DELETE แยกต่างหาก เพราะบางระบบอาจอนุญาตให้อัปโหลดได้แต่ไม่อนุญาตให้ลบไฟล์ของตัวเองทีหลัง ขึ้นกับความต้องการของธุรกิจ

signed URL คืออะไร ใช้ตอนไหนแทน policy ตรง ๆ

บางสถานการณ์ต้องการแชร์ไฟล์ private ให้คนที่ไม่มีบัญชีในระบบเข้าถึงได้ชั่วคราว เช่นส่งลิงก์ดาวน์โหลดใบเสร็จให้ลูกค้าทางอีเมล กรณีแบบนี้เขียน policy อย่างเดียวไม่พอ เพราะผู้รับลิงก์ไม่ได้ล็อกอินเข้าระบบ Supabase Storage แก้ปัญหานี้ด้วย signed URL ซึ่งเป็นลิงก์ที่มีอายุจำกัดและฝัง token ไว้ในตัวเอง

signed URL สร้างขึ้นจากฝั่งเซิร์ฟเวอร์ที่มีสิทธิ์เข้าถึงไฟล์นั้นอยู่แล้ว แล้วส่งลิงก์ให้ client หรือส่งต่อให้ผู้ใช้ปลายทาง เมื่อหมดอายุตามเวลาที่กำหนด ลิงก์นั้นจะใช้เข้าถึงไฟล์ไม่ได้อีก ทำให้ควบคุมได้ว่าใครเข้าถึงไฟล์ได้ในช่วงเวลาไหนโดยไม่ต้องเปิด bucket เป็น public

หลักที่ควรยึดคือใช้ policy ควบคุมการเข้าถึงปกติของผู้ใช้ในระบบ และใช้ signed URL เฉพาะกรณีที่ต้องแชร์ไฟล์ให้คนนอกระบบชั่วคราวเท่านั้น ถ้าต้องการแจ้งเตือนทันทีที่มีไฟล์ใหม่ถูกอัปโหลด สามารถผสานกับ Supabase Realtime ได้เช่นกัน ไม่ใช่ใช้ signed URL แทน policy ทั้งระบบเพราะจะทำให้ดูแลสิทธิ์ยากขึ้นในระยะยาว

ข้อจำกัดเรื่องขนาดไฟล์และประเภทไฟล์ที่ควรตั้งไว้ล่วงหน้า

การเปิดให้ผู้ใช้อัปโหลดไฟล์โดยไม่จำกัดขนาดหรือประเภท เป็นความเสี่ยงทั้งด้านต้นทุนพื้นที่จัดเก็บและด้านความปลอดภัย ควรตั้งค่าจำกัดขนาดไฟล์สูงสุดต่อ bucket และจำกัดประเภทไฟล์ที่อนุญาตไว้ตั้งแต่ระดับ bucket configuration ไม่ใช่พึ่งพาการตรวจสอบฝั่ง frontend เพียงอย่างเดียว เพราะผู้ใช้ที่ตั้งใจข้ามการตรวจสอบสามารถส่ง request ตรงมาที่ API ได้อยู่ดี

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

ปรับขนาดรูปภาพผ่าน Storage โดยไม่ต้องประมวลผลเอง ทำได้ไหม

Supabase Storage มี image transformation ให้ปรับขนาดไฟล์รูปภาพได้ผ่าน query parameter ต่อท้าย URL เช่น ?width=200&height=200&resize=cover&quality=80 โดยไม่ต้องเขียนโค้ดย่อรูปเองฝั่งเซิร์ฟเวอร์หรือประมวลผลก่อนอัปโหลด แต่ใช้ได้เฉพาะไฟล์ประเภทรูปภาพเท่านั้น ไม่ใช่ไฟล์ทุกประเภทที่เก็บใน bucket

resize มีให้เลือกหลายแบบ เช่น cover ครอบตัดภาพให้เต็มขนาดที่กำหนดโดยตัดส่วนที่เกิน contain ย่อภาพให้พอดีกับขนาดที่กำหนดโดยไม่ตัดขอบ และ fill ยืดภาพให้เต็มขนาดโดยไม่รักษาสัดส่วนเดิม ต้องเลือกให้ตรงกับการใช้งานจริง เช่น thumbnail รายการสินค้าที่ต้องการขนาดสม่ำเสมอมักใช้ cover ส่วนภาพหน้าปกบทความที่ไม่อยากให้ส่วนสำคัญถูกตัดออกมักใช้ contain แทน

ค่า quality ยิ่งต่ำยิ่งลดขนาดไฟล์ที่ส่งออกแต่กระทบความคมชัดของภาพ ทีมที่ต้องสร้างหลายขนาดสำหรับ device ต่างกันบ่อย ๆ ควรทดสอบว่าความซับซ้อนของการสร้าง URL หลายแบบต่อรูปเดียวคุ้มค่ากับ performance ที่ได้จริงหรือไม่ ถ้ารูปในระบบมีขนาดพอดีกับการใช้งานตั้งแต่ตอนอัปโหลดอยู่แล้ว ก็อาจไม่จำเป็นต้องพึ่งการ transform เพิ่มทุกครั้งที่แสดงผล

ตัวอย่างสมมติ: คำนวณพื้นที่จัดเก็บและ bandwidth ก่อนตั้ง limit

ตัวอย่างสมมติ (ข้อมูลสมมติ ไม่ใช่ราคาจริงของ Supabase): แอปที่มีผู้ใช้ 2,000 คน แต่ละคนอัปโหลดรูปโปรไฟล์เฉลี่ยคนละ 1.5 MB และเอกสารแนบเฉลี่ยคนละ 3 ไฟล์ ไฟล์ละ 800 KB รวมพื้นที่จัดเก็บโดยประมาณอยู่ที่ 2,000 คูณ (1.5 MB บวก 3 คูณ 800 KB) ซึ่งได้ราว 7.8 GB ถ้าผู้ใช้แต่ละคนเปิดดูไฟล์แนบของตัวเองเฉลี่ยเดือนละ 20 ครั้ง bandwidth ต่อเดือนจากการดาวน์โหลดอย่างเดียวก็อาจสูงกว่าพื้นที่จัดเก็บทั้งหมดหลายเท่าตัว ตัวเลขเหล่านี้เป็นเพียงกรอบคำนวณตัวอย่าง ต้องตรวจสอบหน้า pricing ปัจจุบันของ Supabase ก่อนประเมินต้นทุนจริงเสมอ

ทำแบบนี้แล้วพัง เพราะ...: บางทีมตั้งงบประมาณ Storage จากตัวเลขพื้นที่จัดเก็บอย่างเดียว โดยไม่ได้คำนวณ bandwidth จากการดาวน์โหลดซ้ำ ๆ ของผู้ใช้ พอโปรเจกต์ที่มีคนดูไฟล์บ่อย เช่นแอปแกลเลอรีรูปภาพหรือระบบที่มีคนดาวน์โหลดไฟล์แนบซ้ำหลายรอบขึ้น production จริง ค่าใช้จ่ายจาก bandwidth กลับสูงกว่าค่าใช้จ่ายจากพื้นที่จัดเก็บมาก ควรประเมินทั้งสองตัวแปรแยกกันตั้งแต่ตอนวางแผน ไม่ใช่ดูแค่ขนาดไฟล์รวมอย่างเดียว

ทำแบบนี้แล้วพัง เพราะอะไร

ข้อผิดพลาดที่พบบ่อยเมื่อทีมเริ่มใช้ Supabase Storage:

  • ตั้ง bucket เป็น public ทั้งที่ไฟล์มีข้อมูลส่วนตัว — ใครมี URL ก็เข้าถึงไฟล์ได้ทันทีโดยไม่ต้องผ่านการตรวจสอบสิทธิ์ใด ๆ
  • เปิด RLS บน storage.objects แต่ไม่เขียน policy ให้ครบ — ผู้ใช้ที่ควรอัปโหลดได้กลับเจอ error โดยไม่รู้สาเหตุ เหมือนปัญหาที่เกิดกับตารางข้อมูลทั่วไป
  • ไม่จำกัด path ตอนอัปโหลด — ผู้ใช้คนหนึ่งอัปโหลดไฟล์ไปทับ path ของผู้ใช้อีกคนได้ ถ้า policy ไม่ตรวจสอบว่า path ตรงกับเจ้าของจริง
  • ใช้ signed URL ที่อายุยาวเกินความจำเป็น — ลิงก์ที่หลุดไปอยู่ในมือคนอื่นจะยังใช้เข้าถึงไฟล์ได้นานกว่าที่ควร

สรุป

Supabase Storage ไม่ได้ปลอดภัยเองอัตโนมัติเพียงเพราะอัปโหลดไฟล์เข้าไปในระบบ การควบคุมสิทธิ์ต้องอาศัย RLS policy บนตาราง storage.objects แบบเดียวกับตารางข้อมูลทั่วไป ร่วมกับการเลือกว่า bucket ไหนควรเป็น public หรือ private ให้เหมาะกับลักษณะไฟล์จริง

ทีมที่เพิ่งเริ่มใช้งานควรตั้งกฎตั้งแต่ต้นว่าไฟล์ประเภทไหนเก็บใน bucket ไหน จำกัดขนาดและประเภทไฟล์ให้แคบตามความจำเป็น และใช้ signed URL เฉพาะกรณีต้องแชร์ไฟล์ให้คนนอกระบบชั่วคราวเท่านั้น ไม่ใช่พึ่งพา signed URL แทนการออกแบบสิทธิ์ที่ถูกต้องตั้งแต่แรก

  • metadata ของไฟล์อยู่ในตาราง storage.objects ควบคุมสิทธิ์ด้วย RLS แบบเดียวกับตารางข้อมูลทั่วไป
  • bucket แบบ public ข้ามการตรวจสอบสิทธิ์ทั้งหมด ต้องเลือกใช้เฉพาะไฟล์ที่ตั้งใจเปิดเผยจริง
  • signed URL ใช้แชร์ไฟล์ private ให้คนนอกระบบชั่วคราว ไม่ควรใช้แทน policy ระยะยาว
  • จำกัดขนาดและประเภทไฟล์ตั้งแต่ bucket configuration ไม่พึ่งการตรวจสอบฝั่ง frontend เพียงอย่างเดียว

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

Storage ของ Supabase คิดค่าใช้จ่ายยังไง

คิดตามพื้นที่จัดเก็บและ bandwidth ที่ใช้จริงตาม plan ของโปรเจกต์ ควรตรวจสอบราคาปัจจุบันจากหน้า pricing ของ Supabase โดยตรง เพราะตัวเลขอาจเปลี่ยนแปลงตามช่วงเวลา

อัปโหลดไฟล์ผ่าน Edge Functions ได้ไหม

ได้ Edge Functions สามารถรับไฟล์แล้วอัปโหลดต่อเข้า Storage ด้วย service_role key ได้ เหมาะกับกรณีที่ต้องประมวลผลไฟล์ก่อนบันทึก เช่นย่อขนาดรูปหรือตรวจสอบเนื้อหาก่อน

ลบ bucket ทั้งหมดพร้อมไฟล์ข้างในได้ไหม

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

ไฟล์ที่อัปโหลดซ้ำชื่อเดิมจะเกิดอะไรขึ้น

ขึ้นกับการตั้งค่า upsert ตอนอัปโหลด ถ้าไม่เปิด upsert ระบบจะปฏิเสธและแจ้งว่าไฟล์ชื่อนี้มีอยู่แล้ว ถ้าเปิด upsert ไฟล์เดิมจะถูกเขียนทับด้วยไฟล์ใหม่ ควรเลือกพฤติกรรมนี้ให้ตรงกับความต้องการของฟีเจอร์

ต้องเขียน policy แยกทุก bucket ไหม

ไม่จำเป็นต้องแยกทุก bucket เสมอไป แต่แนะนำให้เขียน policy ที่อ้างอิง bucket_id ในเงื่อนไข เพื่อให้ควบคุมสิทธิ์ต่างกันได้ตามความเสี่ยงของแต่ละ bucket โดยไม่ต้องใช้กฎเดียวกันทั้งหมด

signed URL ปลอดภัยพอสำหรับข้อมูลอ่อนไหวไหม

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

ทดลองวัดผลโฆษณาเข้า LINE ฟรี 14 วัน

สมัครแล้วสร้างโปรเจกต์ เชื่อม LINE ติดตั้ง Tracking แล้วเริ่มเห็น Funnel จาก Click ถึง Purchase ได้ทันที ไม่ต้องใช้บัตรเครดิต

เริ่มทดลองใช้ฟรี 14 วัน

บทความที่เกี่ยวข้อง

Supabase Auth คืออะไร: ออกแบบระบบล็อกอินให้ SaaS ตั้งแต่วันแรก

Supabase Auth คืออะไร: ออกแบบระบบล็อกอินให้ SaaS ตั้งแต่วันแรก

หลายทีมเสียเวลาหลายสัปดาห์เขียนระบบล็อกอินเองตั้งแต่ศูนย์ ทั้งที่มีของสำเร็จรูปที่ปรับแต่งได้ลึกอยู่แล้ว บทความนี้อธิบายว่า Supabase Auth ทำงานยังไงและควรออกแบบ Authentication ของ SaaS อย่างไรให้รองรับการโตในอนาคต
เว็บโดนถล่มตอนเที่ยงคืน ทีมหน้างานเพิ่งรู้ว่า Cloudflare ทำอะไรได้บ้างนอกจาก CDN

เว็บโดนถล่มตอนเที่ยงคืน ทีมหน้างานเพิ่งรู้ว่า Cloudflare ทำอะไรได้บ้างนอกจาก CDN

หลายทีมรู้จัก Cloudflare แค่ในฐานะตัวช่วยกันเว็บล่มกับตัวจัดการ DNS แต่พอเจอเหตุการณ์จริงถึงเห็นว่ามันคือแพลตฟอร์ม Edge ที่ครอบคลุมตั้งแต่ความปลอดภัยไปจนถึง Compute และ AI
Turnstile: ป้องกันบอทแบบไม่ต้องให้ผู้ใช้ติ๊กรูปรถเมล์

Turnstile: ป้องกันบอทแบบไม่ต้องให้ผู้ใช้ติ๊กรูปรถเมล์

CAPTCHA แบบเดิมทำให้ผู้ใช้จริงหงุดหงิดพอ ๆ กับที่กันบอทได้ Turnstile ของ Cloudflare เปลี่ยนวิธีตรวจสอบให้ทำงานเบื้องหลังส่วนใหญ่ แต่ก็ยังมีข้อจำกัดและจุดที่ต้อง verify ฝั่ง server ให้ถูกต้องก่อนเชื่อว่าปลอดภัย