FixVibe

// docs / baas security / supabase storage

Supabase 스토리지 버킷 보안 체크리스트: 22개 항목

Supabase Storage는 S3 호환 버킷에 데이터베이스와 동일한 행 수준 보안 모델을 더한 얇은 래퍼입니다. 즉, 테이블에 영향을 미치는 동일한 RLS 함정이 파일 접근에도 영향을 미치며 — AI 코딩 도구가 업로드를 연결할 때 나타나는 스토리지 특유의 몇 가지도 있습니다. 이 체크리스트는 다섯 섹션의 22개 항목입니다: 버킷 구성, RLS 정책, 업로드 검증, 서명된 URL, 운영 위생. 각 항목은 15분 이내에 검증 가능합니다.

아래 각 항목은 필수입니다. 기본 RLS 메커니즘은 Supabase RLS 스캐너를 참조하세요. 스토리지에 인접한 키 노출 클래스는 JavaScript에 노출된 Supabase 서비스 역할 키를 참조하세요.

버킷 구성

올바른 기본값으로 시작하세요. 잘못 구성된 버킷은 RLS가 올바르든 그렇지 않든 파일을 유출합니다.

  1. 모든 버킷의 기본을 비공개로 설정. Supabase 대시보드 → Storage → Buckets에서 명시적인 이유(마케팅 자산, PII가 없는 공개 아바타)가 없는 한 Public bucket 토글을 끄세요. 공개 버킷은 읽기 작업에 대해 RLS를 우회합니다 — 버킷 이름을 아는 누구나 나열하고 다운로드할 수 있습니다.
  2. 모든 버킷에 엄격한 파일 크기 제한 설정. 대시보드 → 버킷 설정 → 파일 크기 제한. 50 MB는 사용자 업로드에 합리적인 기본값입니다. 비디오 / 대용량 파일 사용 사례에서는 의도적으로 올리세요. 제한이 없으면 단일 악성 업로드가 스토리지 할당량이나 월 대역폭을 소진할 수 있습니다.
  3. 버킷당 허용된 MIME 타입을 제한. 허용된 MIME 타입 목록 — 차단 목록이 아닌 명시적 허용 목록. 이미지 전용 버킷의 경우 image/jpeg, image/png, image/webp. 사용자 콘텐츠 버킷에서는 text/html, application/javascript, 또는 image/svg+xml을 결코 허용하지 마세요 — 서명된 URL을 통해 제공될 때 브라우저에서 실행됩니다.
  4. 하나의 공유 버킷이 아닌 콘텐츠 타입당 하나의 버킷 사용. 버킷별 설정(크기, MIME 타입, RLS 정책)이 당신이 가진 세분성입니다. user-avatars 버킷, document-uploads 버킷, public-assets 버킷이 하나의 혼합 버킷보다 잠그기 쉽습니다.
  5. 프론트엔드가 업로드하는 경우 CORS 구성 확인. 사용자가 브라우저에서 서명된 URL로 직접 업로드하는 경우, 버킷 CORS는 프로덕션 오리진을 나열해야 합니다. *는 공개 버킷에만 허용됩니다 — 사용자 PII를 포함하는 버킷에는 결코 사용하지 마세요.

storage.objects의 RLS 정책

Supabase Storage는 파일 메타데이터를 storage.objects 테이블에 저장합니다. 그 테이블의 RLS가 누가 파일을 읽고, 업로드하고, 업데이트하고, 삭제할 수 있는지 제어합니다. RLS가 없으면 버킷의 공개/비공개 플래그가 유일한 보호입니다.

  1. storage.objects에서 RLS가 활성화되었는지 확인. SELECT rowsecurity FROM pg_tables WHERE schemaname = 'storage' AND tablename = 'objects';true를 반환해야 합니다. Supabase는 새 프로젝트에서 기본적으로 활성화합니다. 비활성화되지 않았는지 확인하세요.
  2. 비공개 버킷에 대해 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()을 사용하여 경로에서 소유자를 추출하는 것입니다.
  3. 동일한 경로 관례를 강제하는 INSERT 정책 작성. CREATE POLICY "users_upload_own" ON storage.objects FOR INSERT WITH CHECK (auth.uid()::text = (storage.foldername(name))[1]);. WITH CHECK가 없으면 인증된 사용자가 다른 사용자의 폴더에 업로드할 수 있습니다.
  4. 앱이 파일 편집이나 삭제를 지원하면 UPDATE 및 DELETE 정책 추가. 각 명령은 자체 정책이 필요합니다. DELETE를 건너뛰면 인증된 사용자가 자신의 파일을 제거할 수 없습니다. UPDATE를 건너뛰면 파일 덮어쓰기가 조용히 실패합니다.
  5. 두 개의 브라우저 세션에서 교차 사용자 접근 테스트. 사용자 A로 로그인하고 파일을 업로드한 다음 경로를 복사합니다. 다른 브라우저에서 사용자 B로 로그인하고 REST API를 통해 파일을 가져오려고 시도하세요. 응답은 403 또는 404여야 하며 결코 200이 되어서는 안 됩니다.
sql
-- 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 코딩 도구는 기본적으로 클라이언트 전용 검증을 생성하며, 그것은 아무것도 보호하지 않습니다.

  1. MIME 타입을 Content-Type 헤더가 아닌 파일의 실제 바이트에서 서버 측에서 재확인. file-type(Node) 같은 라이브러리나 매직 바이트 스니핑을 사용하세요. 공격자는 실제로 폴리글로트 HTML / JavaScript 페이로드인 파일에 대해 Content-Type: image/jpeg을 주장할 수 있습니다.
  2. 업로드된 이미지에서 EXIF 메타데이터 제거. EXIF는 GPS 좌표, 기기 일련번호, 타임스탬프를 포함할 수 있습니다. 저장 전에 sharp.withMetadata(false) 또는 exif-parser를 사용하여 제거하세요.
  3. script 태그나 onload 핸들러를 포함하는 SVG 거부. SVG는 XML이며 — 많은 AI 생성 앱은 SVG 업로드를 "그냥 이미지"로 허용합니다. 서버 측에서 DOMPurify를 사용하거나 SVG 업로드를 완전히 거부하세요.
  4. 결정론적이고 추측할 수 없는 파일 이름 사용. 원본 파일 이름을 보존하지 마세요. UUID나 파일 내용의 해시를 사용하세요. 원본 파일 이름은 유출을 일으키며("passport_scan_2024_01_15.jpg"), 예측 가능한 이름은 열거를 가능하게 합니다.

서명된 URL

서명된 URL은 클라이언트가 비공개 버킷에 접근하는 방법입니다. 만료, 버킷 범위, 그리고 로깅되는 내용이 중요합니다.

  1. 서명된 URL 만료를 기본 1시간 이하로 설정. Supabase JS SDK의 createSignedUrl(path, expiresIn)은 초를 받습니다. 31536000(1년) 같은 값을 결코 사용하지 마세요 — URL이 영구적인 반-공개 링크가 됩니다.
  2. 서명된 URL을 데이터베이스에 저장하지 마세요. 모든 요청에서 서버 측에서 새로 생성하세요. 1년 만료의 저장된 서명된 URL이 데이터베이스 덤프를 통해 유출되면 장기적 접근을 부여합니다.
  3. 파일 업로드뿐 아니라 서명된 URL 생성도 로그. 나중에 침해를 의심한다면 누가 언제 어떤 URL을 생성했는지 알아야 합니다. auth.uid() + 버킷 + 객체 경로 + 타임스탬프를 로그하세요.
  4. 사용자 업로드 파일을 제공할 때 downloadAs 옵션 사용. createSignedUrl(path, expiresIn, { download: '.jpg' })Content-Disposition: attachment 헤더를 강제하므로 파일이 렌더링되는 대신 다운로드됩니다 — HTML / SVG / PDF 내 HTML 실행 클래스를 방지합니다.

운영 위생

스토리지 구성은 시간이 지나면서 드리프트합니다. 이 네 가지 운영 항목은 표면을 단단하게 유지합니다.

  1. 분기별로 버킷 감사. 대시보드 → Storage → Buckets. 공개/비공개 상태와 MIME 타입 목록이 앱이 기대하는 것과 일치하는지 확인하세요. "임시로" 생성된 버킷은 누구도 제거하지 않으면 영구가 됩니다.
  2. 익명 목록 작업 모니터링. 스토리지 로그(대시보드 → Logs → Storage)는 LIST 요청을 기록합니다. 비공개 버킷에 대한 익명 목록 요청 급증은 누군가가 외부에서 탐색하고 있다는 의미입니다.
  3. 임시 업로드에 대한 보존 정책 설정. 임시 버킷(이미지 미리보기, 초안 업로드)은 예약된 함수를 통해 24-72시간 후에 자동 삭제되어야 합니다. 무기한 보존은 GDPR / CCPA 데이터 최소화 의무 하에서 책임입니다.
  4. 월별로 FixVibe 스캔 실행. baas.supabase-storage-public 검사는 익명 GET + LIST에 응답하는 버킷을 탐사합니다. 새 버킷이 추가되고 오래된 것은 가시성이 변경됩니다 — 지속적인 스캔만이 드리프트를 잡습니다.

다음 단계

프로덕션 URL에 대해 FixVibe 스캔을 실행하세요 — 익명 스토리지 목록은 baas.supabase-storage-public 아래에 표시됩니다. 테이블 계층은 Supabase RLS 스캐너와 결합하고, 키 노출 인접성은 JavaScript에 노출된 Supabase 서비스 역할 키와 결합하세요. 다른 BaaS 제공자의 스토리지 설정 오류는 BaaS 설정 오류 스캐너를 참조하세요.

// baas 표면 스캔

다른 누군가가 발견하기 전에 열린 테이블을 찾으세요.

프로덕션 URL을 입력하세요. FixVibe는 앱이 통신하는 BaaS 제공자를 열거하고, 공개 엔드포인트의 지문을 채취하며, 인증되지 않은 클라이언트가 무엇을 읽거나 쓸 수 있는지 보고합니다. 무료, 설치 불필요, 카드 불필요.

  • 무료 티어 — 월 3회 스캔, 가입 시 카드 불필요.
  • 수동 BaaS 지문 채취 — 도메인 소유권 확인 불필요.
  • Supabase, Firebase, Clerk, Auth0, Appwrite 등.
  • 모든 발견에 AI 수정 프롬프트 — Cursor / Claude Code에 그대로 붙여넣기.
Supabase 스토리지 버킷 보안 체크리스트: 22개 항목 — Docs · FixVibe