전체 가이드
심각 심각도데이터베이스 보안

Supabase Row Level Security(RLS): 테이블을 확실하게 잠그기

Supabase는 Data API를 통해 Postgres 테이블을 브라우저에 직접 노출하며, 공개 anon(publishable) key로 인증합니다. 각 사용자가 어떤 행을 읽거나 변경할 수 있는지는 Row Level Security(RLS) 정책이 결정합니다. public 스키마에 있으면서 RLS가 없는 테이블은 사이트를 여는 누구나 읽을 수 있고, 많은 경우 쓸 수도 있습니다.

무엇인가요

모든 Supabase 프로젝트에는 프런트엔드에 두도록 만들어진 anon(또는 sb_publishable_) key가 있습니다. 이 키는 사용자가 아니라 프로젝트를 식별합니다 — 누구나 JavaScript 번들에서 복사해 UI를 완전히 우회하고 https://<project>.supabase.co/rest/v1/로 직접 요청을 보낼 수 있습니다.

Row Level Security는 테이블에 정책을 붙이는 Postgres 기능입니다. 정책은 모든 SELECT, INSERT, UPDATE, DELETE에서 각 행마다 검사되는 SQL 조건입니다. RLS를 활성화하고 정책이 없으면 API로는 아무것도 접근할 수 없습니다. 정책을 하나 추가할 때마다 문이 정확히 하나씩 열립니다 — 예를 들어 “로그인한 사용자는 user_id가 자신의 id와 일치하는 행을 읽을 수 있다”처럼요.

왜 중요한가요

정책이 없거나 지나치게 넓은 것은 사소한 버그가 아니라 전면적인 데이터 유출입니다. 사용자 이메일, 프로필, 주문, 비공개 메시지 — 테이블에 있는 것은 무엇이든 — curl 명령 한 줄로 빼낼 수 있습니다. AI 코딩 도구는 SQL 마이그레이션으로 테이블을 만드는 경우가 많은데, 이때 RLS는 기본적으로 꺼져 있습니다. 또한 앱을 동작시키려고 using (true) 같은 느슨한 임시 정책을 생성하곤 합니다.

service_role(sb_secret_) key는 RLS를 완전히 우회합니다. 이 키가 클라이언트 측 코드에 들어가면 여러분이 작성한 모든 정책이 무의미해집니다 — 키를 가진 누구나 데이터베이스에 대한 전체 읽기/쓰기 권한을 갖게 됩니다.

고치는 방법

RLS가 없는 테이블 찾기

Supabase SQL 편집기에서 이 쿼리를 실행해, 노출된 public 스키마에서 아직 RLS가 꺼져 있는 모든 테이블을 나열하세요. 대시보드의 Security Advisor도 같은 문제를 “RLS disabled in public”으로 보고합니다.

select tablename
from pg_tables
where schemaname = 'public'
  and rowsecurity = false;
RLS를 활성화하고 소유자 전용 정책 작성하기

각 테이블에서 RLS를 켜고, 작업마다 정책을 하나씩 추가하세요. 정책 대상을 authenticated 역할로 한정하고, 행의 소유자 컬럼을 auth.uid()와 비교하세요. INSERT와 UPDATE에는 with check가 필요합니다. 그래야 사용자가 다른 사람 소유의 행을 쓸 수 없습니다.

alter table public.profiles enable row level security;

create policy "Users read own profile"
  on public.profiles for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "Users update own profile"
  on public.profiles for update
  to authenticated
  using ((select auth.uid()) = user_id)
  with check ((select auth.uid()) = user_id);
흔한 정책 실수 피하기

using (true) 같은 임시 정책을 실제 조건으로 바꾸세요. 사용자가 직접 수정할 수 있는 user_metadata를 권한 판단의 근거로 삼지 말고, 대신 app_metadata나 역할 테이블을 사용하세요. 뷰는 security_invoker = true로 생성하지 않으면 RLS를 우회하며, 스토리지 버킷에는 storage.objects에 대한 별도 정책이 필요합니다.

anon 키로 외부에서 테스트하기

공격자와 똑같은 방식으로 API를 조회하세요. 즉, 공개 키만 사용하고 사용자 세션은 없이요. 공개되면 안 되는 행이 반환된다면 정책이 너무 열려 있는 것입니다.

curl "https://<project>.supabase.co/rest/v1/profiles?select=*" \
  -H "apikey: <anon-key>" \
  -H "Authorization: Bearer <anon-key>"
service_role 키는 서버에 두기

service_role(sb_secret_) key는 API route나 Edge Functions 같은 서버 코드에서만 사용하고, 절대 NEXT_PUBLIC_이나 VITE_ 접두사 뒤에 두지 마세요. 한 번이라도 브라우저로 전달된 적이 있다면 대시보드에서 교체하세요.

자주 묻는 질문

Supabase anon 키가 브라우저에서 보여도 안전한가요?

네 — anon(publishable) key는 공개되도록 설계되었습니다. 단, 노출된 모든 테이블에 RLS가 활성화되어 있고 올바른 정책이 있을 때만 그렇습니다. 키 자체는 정책이 허용하지 않는 것은 아무것도 허용하지 않습니다.

RLS를 활성화했더니 앱이 빈 결과만 반환합니다. 고장 난 건가요?

RLS가 의도대로 동작하는 것입니다. 정책이 없으면 모든 행이 거부됩니다. RLS를 다시 끄지 말고, 앱에 필요한 작업에 대해 적절한 사용자로 범위를 한정한 정책을 추가하세요.

AppSafe가 RLS 정책을 테스트하나요?

AppSafe는 데이터베이스에 로그인하지 않으므로 정책을 평가할 수 없습니다 — 위의 SQL과 curl 점검을 사용하세요. 다만 JavaScript에서 Supabase 키는 스캔합니다. 키가 보고되면 JWT의 가운데 부분을 디코딩해 role 클레임을 확인하세요. anon은 브라우저에 있어도 정상이지만, service_role은 심각한 유출입니다.

내 앱도 영향을 받나요?

AppSafe는 이 문제와 수십 가지 다른 문제를 한 번의 무료 스캔으로 점검합니다.

내 앱 무료 스캔