Supabase RLS 감사
이 스킬은 vibe-preflight 스캐너의 데이터베이스 부분을 실행합니다(scripts/preflight.mjs, 이 파일과 동일한 위치, Node 20+, 의존성 없음, 네트워크 필요 없음). 프로젝트의 .sql 파일(Supabase 마이그레이션, schema.sql)을 읽어 테이블, 정책, 뷰, 함수, 권한 부여, 스토리지 버킷을 순서대로 재생하고 9가지 문제를 보고합니다. 데이터베이스에 연결하지 않습니다.
실행하기
프로젝트 루트에서 다음을 실행합니다:
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only database --format json
Claude Code는 ${CLAUDE_SKILL_DIR}을 자동으로 채웁니다. 다른 에이전트: 이 스킬의 폴더를 사용하세요. 예: .agents/skills/supabase-rls-audit 또는 .claude/skills/supabase-rls-audit.
종료 코드 1은 하나 이상의 오류가 있음을 의미합니다. --fail-on warning을 추가하면 경고에도 실패 처리하고, --explain(텍스트 출력)을 추가하면 각 문제의 원인과 해결 방법을 설명합니다. JSON: { summary: { error, warning }, findings: [{ rule, severity, message, file, line }] }.
결과가 0 SQL files라고 나오면 정책이 대시보드에만 있을 가능성이 높습니다. 사용자에게 supabase db pull을 실행하도록 요청하세요(원격 스키마를 마이그레이션 파일로 작성합니다). 빈 스캔 결과를 프로젝트가 깨끗하다고 보고하지 마세요.
검사 항목
| 규칙 | 수준 | 의미 |
|---|---|---|
rls-disabled | 오류 | public 테이블에 enable row level security가 설정되지 않아 anon 키를 가진 누구나 API를 통해 읽고 쓸 수 있습니다. |
rls-permissive-policy | 쓰기: 오류, 읽기: 경고 | using (true) 또는 with check (true). 삽입/업데이트/삭제 시 모든 사용자가 다른 사용자의 행을 변경할 수 있습니다. 선택 시 모든 행이 공개됩니다. |
rls-no-policy | 경고 | RLS는 활성화되어 있지만 정책이 없음: API가 모든 것을 거부합니다. 서버 전용 테이블에는 괜찮지만, 브라우저에서 읽으면 빈 목록이 반환됩니다. |
rls-update-privileged-column | 경고 | 사용자가 자신의 행을 업데이트할 수 있고 해당 행에 plan, role, is_admin, credits 등이 포함되어 있습니다. RLS는 행을 제한할 뿐 열을 제한하지 않습니다. |
view-bypasses-rls | 오류 | security_invoker = true가 없는 뷰는 소유자 권한으로 실행되어 하위 테이블의 RLS를 우회합니다. |
auth-users-exposed | 오류 | public 스키마의 뷰가 auth.users를 선택합니다. |
definer-search-path | 경고 | set search_path 없이 security definer 함수가 있습니다. |
definer-function-anon | 오류 / 경고 | public 스키마에 security definer 함수가 있고 anon이 /rest/v1/rpc/<name>으로 호출할 수 있습니다. anon에 명시적 권한 부여가 있으면 오류, Supabase 기본 권한 부여이면 경고입니다. |
storage-public-bucket | 오류 / 경고 | 공개 버킷입니다. storage.objects에 대한 select 정책이 누구나 나열할 수 있도록 하면 오류입니다. |
해결 방법
항상 새로운 마이그레이션을 작성하세요. 이미 데이터베이스에서 실행된 마이그레이션은 절대 수정하지 마세요.
누락되었거나 허용적인 정책: 앱에 필요한 접근 권한을 물어본 후 각 정책을 역할과 소유자로 범위를 지정하세요.
alter table public.notes enable row level security;
create policy "notes: read own" on public.notes
for select to authenticated using ((select auth.uid()) = user_id);
create policy "notes: insert own" on public.notes
for insert to authenticated with check ((select auth.uid()) = user_id);
권한 있는 열: 사용자가 변경할 수 있는 열에 대해서만 업데이트를 허용하세요.
revoke update on public.profiles from authenticated;
grant update (display_name, avatar_url) on public.profiles to authenticated;
뷰: alter view public.<name> set (security_invoker = true); (Postgres 15+, 현재 모든 Supabase 프로젝트). 구체화된 뷰는 RLS를 사용할 수 없습니다: public 밖으로 이동하거나 anon, authenticated의 select 권한을 취소하세요. auth.users의 경우, 가입 트리거를 사용하여 공유 가능한 필드를 public.profiles로 복사하세요.
SECURITY DEFINER 함수: set search_path = ''를 추가하고 모든 이름을 스키마와 함께 작성하세요. 그런 다음 호출할 수 있는 사용자를 제한하세요:
revoke execute on function public.admin_set_plan(uuid, text) from public, anon, authenticated;
grant execute on function public.admin_set_plan(uuid, text) to service_role;
Supabase에서는 public에서만 취소하는 것만으로는 충분하지 않습니다. anon이 새 함수에 대해 자체 권한을 가지고 있기 때문입니다.
스토리지: 비공개 버킷을 비공개로 만들고, 읽기 범위를 소유자 폴더로 지정하세요((storage.foldername(name))[1] = (select auth.uid())::text), 그리고 서명된 URL을 제공하세요. 제품 이미지와 로고는 공개로 유지해도 됩니다.
의도된 발견 사항(공개 카탈로그, 서버 전용 테이블)은 해당 줄 또는 바로 위 줄에 -- vibe-preflight-ignore <rule-id>를 추가하여 무음 처리할 수 있습니다. 같은 주석에 이유를 작성하세요.
보고
- 보고된 각 줄을 열어 SQL과 대조하여 확인하세요. 스캐너는 SQL 텍스트를 읽으며 오류가 있을 수 있습니다. 오류가 있으면 그렇게 말하세요.
- 요약을 규칙별로 그룹화하세요. 오류를 먼저, 그 다음 사용자와 확인한 경고를 나열하세요.
- 수정 후 스캔을 다시 실행하고 새 결과를 보고하세요.
한계
.sql파일에 있는 내용만 검사합니다. 대시보드에서만 변경한 사항, 다른 스키마, 함수 본문 내부 동작은 검사되지 않습니다.- 깨끗한 결과는 이 9가지 패턴이 발견되지 않았음을 의미합니다. 정책이 앱의 규칙과 일치한다는 것을 증명하지는 않습니다. 실제 anon 및 로그인 사용자로 테스트하는 것(Supabase 클라이언트와 anon 키 사용)은 여전히 가치가 있습니다.
- 브라우저 및 환경 측(클라이언트 코드의 service_role 키,
NEXT_PUBLIC_변수의 시크릿, 커밋된.env)의 경우--only없이 전체 스캔을 실행하거나vibe-preflight스킬을 사용하세요.