Supabase Storage 보안 체크리스트: 버킷·RLS·파일 업로드 10가지
프로필 사진, 영수증, 계약서, 포트폴리오 파일을 받기 시작하면 데이터베이스 RLS만으로는 충분하지 않습니다. Supabase Storage는 파일 자체와 storage.objects 메타데이터에 별도의 접근 규칙이 필요합니다. 화면에서 업로드 버튼을 숨겼다고 다른 사용자의 파일 접근이 막히는 것도 아닙니다.
1. public과 private 버킷을 데이터 성격에 맞게 나눴는가
public 버킷의 파일은 주소를 아는 누구나 조회할 수 있습니다. 프로필 이미지나 공개 게시물 첨부파일에는 맞을 수 있지만, “주소가 길어서 아무도 모를 것”이라는 가정으로 개인정보를 넣으면 안 됩니다. private 버킷은 다운로드에도 권한 검사가 적용되므로 인증된 다운로드나 짧은 signed URL을 사용합니다.
2. storage.objects 정책을 작업별로 확인했는가
업로드는 INSERT, 조회는 SELECT, 덮어쓰기는 UPDATE, 삭제는 DELETE 정책과 연결됩니다. “authenticated 사용자는 모두 허용”처럼 넓은 정책 하나로 끝내지 말고 실제 기능에서 필요한 작업만 허용하세요. Supabase 공식 문서는 upsert에 추가 권한이 필요할 수 있음을 안내합니다.
3. 사용자별 폴더 경계를 서버가 아닌 RLS에서도 검사하는가
user-a/avatar.png 같은 경로 규칙을 프런트엔드 코드에서만 만들면 요청을 직접 바꿔 다른 폴더에 업로드할 수 있습니다. 첫 번째 폴더가 로그인 사용자의 ID와 일치하는지 정책에서 검사하고, 읽기·수정·삭제에도 같은 소유권 조건이 적용되는지 확인하세요.
4. 다른 사용자의 경로를 추측해 직접 요청해보았는가
테스트 계정 두 개를 만들고 A가 올린 파일 경로를 B 계정으로 조회·교체·삭제해 보세요. 목록 화면에서 파일이 안 보이는 것만으로는 부족합니다. Storage API 요청을 직접 보내거나 개발자 도구에서 기존 요청의 경로와 토큰을 바꿔 실제 응답이 거부되는지 확인해야 합니다.
5. service role 키가 클라이언트에 포함되지 않았는가
service role은 RLS를 우회할 수 있습니다. 브라우저 자바스크립트, 모바일 앱 번들, 공개 Git 저장소와 VITE_·NEXT_PUBLIC_ 환경변수에 넣으면 안 됩니다. 관리자 업로드나 백그라운드 처리가 필요하면 서버 함수에 보관하고, 서버에서도 사용자 요청값을 그대로 신뢰하지 마세요.
6. 허용 파일 형식과 최대 크기를 버킷에 설정했는가
Supabase 버킷에는 최대 파일 크기와 허용 MIME 형식을 정할 수 있습니다. 이미지 기능이라면 실제 필요한 형식만 허용하고, 영상·압축파일·실행 가능한 파일을 무심코 열어두지 마세요. 클라이언트 검사만 사용하면 우회할 수 있으므로 버킷과 서버에서도 제한해야 합니다.
7. 확장자와 Content-Type 외에 실제 파일도 검증하는가
image/jpeg라는 요청 헤더나 .jpg 확장자만으로 실제 이미지라고 단정할 수 없습니다. OWASP는 허용 확장자 목록, Content-Type, 파일 서명, 안전한 파일명과 크기 제한을 함께 적용하라고 권고합니다. 문서 업로드처럼 위험도가 높은 기능은 악성 파일 검사나 별도 변환 과정도 검토하세요.
8. 원본 파일명을 그대로 저장하지 않는가
사용자 파일명에는 경로 구분 문자, 너무 긴 이름, 특수문자나 개인정보가 들어갈 수 있습니다. 저장 키는 애플리케이션이 만든 UUID처럼 예측하기 어려운 값으로 바꾸고, 원래 이름이 필요하면 화면 표시용 메타데이터로 분리하세요. 같은 이름의 파일이 기존 파일을 덮어쓰는지도 확인해야 합니다.
9. signed URL의 유효기간과 노출 경로를 제한했는가
private 파일을 공유할 때 signed URL은 유효기간 동안 접근을 허용합니다. 계약서처럼 민감한 파일에 지나치게 긴 만료 시간을 주지 말고, 분석 로그·공개 채팅·검색 가능한 문서에 URL이 남지 않게 하세요. 사용자가 권한을 잃거나 파일을 삭제한 뒤 기존 링크가 어떻게 동작하는지도 시험합니다.
10. 업로드 실패·삭제·탈퇴 흐름까지 테스트했는가
업로드 도중 네트워크가 끊기면 고아 파일이 남는지, 데이터베이스 행 삭제와 Storage 파일 삭제가 함께 처리되는지 봅니다. 회원 탈퇴 때 보관해야 할 파일과 지워야 할 파일을 구분하고, 삭제 실패를 기록해 재처리할 경로를 준비하세요. 메타데이터만 직접 지우면 실제 저장 객체와 불일치가 생길 수 있으므로 Storage API를 사용합니다.
출시 전 계정 2개로 실행할 테스트
| 시나리오 | 기대 결과 | 실패 시 의심할 곳 |
|---|---|---|
| A가 자신의 폴더에 허용 이미지 업로드 | 성공 | INSERT·SELECT 정책, MIME·크기 제한 |
| A가 B의 폴더명으로 업로드 | 403 또는 정책 거부 | 폴더 소유권 with check |
| B가 A의 private 파일 다운로드 | 403 또는 접근 거부 | SELECT 정책, signed URL 발급 로직 |
| B가 A 파일을 덮어쓰기·삭제 | 거부 | UPDATE·DELETE 소유권 정책 |
| 허용되지 않은 형식·초과 크기 업로드 | 업로드 전 또는 서버에서 거부 | 버킷 제한, 서버 검증 |
| 만료된 signed URL 열기 | 접근 거부 | URL 만료 설정·캐시 |
공식 참고 자료
- Supabase Storage Access Control —
storage.objectsRLS와 작업별 정책 - Supabase Storage Buckets — public·private 버킷, 파일 크기와 MIME 제한
- Supabase Storage Ownership —
owner_id와 소유권 기반 접근 제어 - OWASP File Upload Cheat Sheet — 파일 형식·서명·이름·크기와 저장 위치 검증
함께 볼 가이드
- Supabase Auth 보안 체크리스트
- Supabase RLS 보안 체크리스트
- Lovable 앱 출시 전 체크리스트
- .env와 서버 키 노출 확인법
- Vercel 환경변수·비밀키 보안 체크리스트
자주 묻는 질문
Supabase public 버킷에도 RLS가 적용되나요?
public 버킷은 파일 조회·제공이 공개되지만 업로드, 수정, 이동과 삭제 같은 작업에는 접근 제어가 계속 필요합니다. 개인정보나 비공개 문서는 private 버킷과 사용자별 RLS 정책을 사용하세요.
service_role 키를 브라우저에서 써도 되나요?
안 됩니다. service role은 Storage RLS를 우회할 수 있으므로 신뢰할 수 있는 서버 환경에만 보관해야 합니다. 브라우저 번들, 공개 저장소와 클라이언트 환경변수에 포함되면 즉시 폐기하고 재발급해야 합니다.
파일 확장자와 Content-Type만 검사하면 안전한가요?
충분하지 않습니다. Content-Type은 조작될 수 있으므로 허용 확장자, 실제 파일 서명, 크기, 파일명, 사용자 권한과 필요 시 악성 파일 검사까지 여러 단계로 확인해야 합니다.
signed URL이면 파일이 영구 공개되지 않나요?
signed URL은 정해진 유효기간 동안 접근을 허용하는 방식입니다. 다만 유효기간 안에는 URL을 받은 사람이 열 수 있으므로 민감도에 맞게 시간을 짧게 정하고 로그나 공개 문서에 남지 않게 관리하세요.