// docs / baas security / supabase storage
Supabase 스토리지 버킷 보안 체크리스트: 22개 항목
Supabase Storage는 S3 호환 버킷에 데이터베이스와 동일한 행 수준 보안 모델을 더한 얇은 래퍼입니다. 즉, 테이블에 영향을 미치는 동일한 RLS 함정이 파일 접근에도 영향을 미치며 — AI 코딩 도구가 업로드를 연결할 때 나타나는 스토리지 특유의 몇 가지도 있습니다. 이 체크리스트는 다섯 섹션의 22개 항목입니다: 버킷 구성, RLS 정책, 업로드 검증, 서명된 URL, 운영 위생. 각 항목은 15분 이내에 검증 가능합니다.
아래 각 항목은 필수입니다. 기본 RLS 메커니즘은 Supabase RLS 스캐너를 참조하세요. 스토리지에 인접한 키 노출 클래스는 JavaScript에 노출된 Supabase 서비스 역할 키를 참조하세요.
버킷 구성
올바른 기본값으로 시작하세요. 잘못 구성된 버킷은 RLS가 올바르든 그렇지 않든 파일을 유출합니다.
- 모든 버킷의 기본을 비공개로 설정. Supabase 대시보드 → Storage → Buckets에서 명시적인 이유(마케팅 자산, PII가 없는 공개 아바타)가 없는 한 Public bucket 토글을 끄세요. 공개 버킷은 읽기 작업에 대해 RLS를 우회합니다 — 버킷 이름을 아는 누구나 나열하고 다운로드할 수 있습니다.
- 모든 버킷에 엄격한 파일 크기 제한 설정. 대시보드 → 버킷 설정 → 파일 크기 제한. 50 MB는 사용자 업로드에 합리적인 기본값입니다. 비디오 / 대용량 파일 사용 사례에서는 의도적으로 올리세요. 제한이 없으면 단일 악성 업로드가 스토리지 할당량이나 월 대역폭을 소진할 수 있습니다.
- 버킷당 허용된 MIME 타입을 제한. 허용된 MIME 타입 목록 — 차단 목록이 아닌 명시적 허용 목록. 이미지 전용 버킷의 경우
image/jpeg,image/png,image/webp. 사용자 콘텐츠 버킷에서는text/html,application/javascript, 또는image/svg+xml을 결코 허용하지 마세요 — 서명된 URL을 통해 제공될 때 브라우저에서 실행됩니다. - 하나의 공유 버킷이 아닌 콘텐츠 타입당 하나의 버킷 사용. 버킷별 설정(크기, MIME 타입, RLS 정책)이 당신이 가진 세분성입니다.
user-avatars버킷,document-uploads버킷,public-assets버킷이 하나의 혼합 버킷보다 잠그기 쉽습니다. - 프론트엔드가 업로드하는 경우 CORS 구성 확인. 사용자가 브라우저에서 서명된 URL로 직접 업로드하는 경우, 버킷 CORS는 프로덕션 오리진을 나열해야 합니다.
*는 공개 버킷에만 허용됩니다 — 사용자 PII를 포함하는 버킷에는 결코 사용하지 마세요.
storage.objects의 RLS 정책
Supabase Storage는 파일 메타데이터를 storage.objects 테이블에 저장합니다. 그 테이블의 RLS가 누가 파일을 읽고, 업로드하고, 업데이트하고, 삭제할 수 있는지 제어합니다. RLS가 없으면 버킷의 공개/비공개 플래그가 유일한 보호입니다.
- storage.objects에서 RLS가 활성화되었는지 확인.
SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects';은true를 반환해야 합니다. Supabase는 새 프로젝트에서 기본적으로 활성화합니다. 비활성화되지 않았는지 확인하세요. - 비공개 버킷에 대해
auth.uid()로 범위가 좁혀진 SELECT 정책 작성.CREATE POLICY "users_read_own_files" ON storage.objects FOR SELECT USING (auth.uid()::text = (storage.foldername(name))[1]);. 관례는 파일을[user-id]/[filename]하위에 저장하고storage.foldername()을 사용하여 경로에서 소유자를 추출하는 것입니다. - 동일한 경로 관례를 강제하는 INSERT 정책 작성.
CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. WITH CHECK가 없으면 인증된 사용자가 다른 사용자의 폴더에 업로드할 수 있습니다. - 앱이 파일 편집이나 삭제를 지원하면 UPDATE 및 DELETE 정책 추가. 각 명령은 자체 정책이 필요합니다. DELETE를 건너뛰면 인증된 사용자가 자신의 파일을 제거할 수 없습니다. UPDATE를 건너뛰면 파일 덮어쓰기가 조용히 실패합니다.
- 두 개의 브라우저 세션에서 교차 사용자 접근 테스트. 사용자 A로 로그인하고 파일을 업로드한 다음 경로를 복사합니다. 다른 브라우저에서 사용자 B로 로그인하고 REST API를 통해 파일을 가져오려고 시도하세요. 응답은
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]);업로드 검증
버킷에 MIME 및 크기 제약이 있더라도 모든 업로드를 서버 측에서 검증하세요. AI 코딩 도구는 기본적으로 클라이언트 전용 검증을 생성하며, 그것은 아무것도 보호하지 않습니다.
- MIME 타입을
Content-Type헤더가 아닌 파일의 실제 바이트에서 서버 측에서 재확인.file-type(Node) 같은 라이브러리나 매직 바이트 스니핑을 사용하세요. 공격자는 실제로 폴리글로트 HTML / JavaScript 페이로드인 파일에 대해Content-Type: image/jpeg을 주장할 수 있습니다. - 업로드된 이미지에서 EXIF 메타데이터 제거. EXIF는 GPS 좌표, 기기 일련번호, 타임스탬프를 포함할 수 있습니다. 저장 전에
sharp의.withMetadata(false)또는exif-parser를 사용하여 제거하세요. script태그나onload핸들러를 포함하는 SVG 거부. SVG는 XML이며 — 많은 AI 생성 앱은 SVG 업로드를 "그냥 이미지"로 허용합니다. 서버 측에서DOMPurify를 사용하거나 SVG 업로드를 완전히 거부하세요.- 결정론적이고 추측할 수 없는 파일 이름 사용. 원본 파일 이름을 보존하지 마세요. UUID나 파일 내용의 해시를 사용하세요. 원본 파일 이름은 유출을 일으키며("
passport_scan_2024_01_15.jpg"), 예측 가능한 이름은 열거를 가능하게 합니다.
서명된 URL
서명된 URL은 클라이언트가 비공개 버킷에 접근하는 방법입니다. 만료, 버킷 범위, 그리고 로깅되는 내용이 중요합니다.
- 서명된 URL 만료를 기본 1시간 이하로 설정. Supabase JS SDK의
createSignedUrl(path, expiresIn)은 초를 받습니다.31536000(1년) 같은 값을 결코 사용하지 마세요 — URL이 영구적인 반-공개 링크가 됩니다. - 서명된 URL을 데이터베이스에 저장하지 마세요. 모든 요청에서 서버 측에서 새로 생성하세요. 1년 만료의 저장된 서명된 URL이 데이터베이스 덤프를 통해 유출되면 장기적 접근을 부여합니다.
- 파일 업로드뿐 아니라 서명된 URL 생성도 로그. 나중에 침해를 의심한다면 누가 언제 어떤 URL을 생성했는지 알아야 합니다.
auth.uid()+ 버킷 + 객체 경로 + 타임스탬프를 로그하세요. - 사용자 업로드 파일을 제공할 때
downloadAs옵션 사용.createSignedUrl(path, expiresIn, { download: '.jpg' })은Content-Disposition: attachment헤더를 강제하므로 파일이 렌더링되는 대신 다운로드됩니다 — HTML / SVG / PDF 내 HTML 실행 클래스를 방지합니다.
운영 위생
스토리지 구성은 시간이 지나면서 드리프트합니다. 이 네 가지 운영 항목은 표면을 단단하게 유지합니다.
- 분기별로 버킷 감사. 대시보드 → Storage → Buckets. 공개/비공개 상태와 MIME 타입 목록이 앱이 기대하는 것과 일치하는지 확인하세요. "임시로" 생성된 버킷은 누구도 제거하지 않으면 영구가 됩니다.
- 익명 목록 작업 모니터링. 스토리지 로그(대시보드 → Logs → Storage)는
LIST요청을 기록합니다. 비공개 버킷에 대한 익명 목록 요청 급증은 누군가가 외부에서 탐색하고 있다는 의미입니다. - 임시 업로드에 대한 보존 정책 설정. 임시 버킷(이미지 미리보기, 초안 업로드)은 예약된 함수를 통해 24-72시간 후에 자동 삭제되어야 합니다. 무기한 보존은 GDPR / CCPA 데이터 최소화 의무 하에서 책임입니다.
- 월별로 FixVibe 스캔 실행.
baas.supabase-storage-public검사는 익명GET+LIST에 응답하는 버킷을 탐사합니다. 새 버킷이 추가되고 오래된 것은 가시성이 변경됩니다 — 지속적인 스캔만이 드리프트를 잡습니다.
다음 단계
프로덕션 URL에 대해 FixVibe 스캔을 실행하세요 — 익명 스토리지 목록은 baas.supabase-storage-public 아래에 표시됩니다. 테이블 계층은 Supabase RLS 스캐너와 결합하고, 키 노출 인접성은 JavaScript에 노출된 Supabase 서비스 역할 키와 결합하세요. 다른 BaaS 제공자의 스토리지 설정 오류는 BaaS 설정 오류 스캐너를 참조하세요.
