제공 서비스
웹 보안 점검 · 무료 단건 외부 공격표면 관리 · 지속 자동 AI 취약점 진단 · 신규 모의해킹 · 침투 테스트
요금제 스토어 블로그 파트너 마이페이지 무료 보안 점검
가이드 2026.02.18 · 조회 4220

Supabase RLS 설정 실수 방지법

Supabase RLS를 제대로 설정하지 않으면 모든 사용자 데이터가 노출된다. 실수 패턴과 올바른 설정법을 정리했다.

S
보안 전문 기업 SENTRIX 레드팀
해킹대회 수상 · 보안 실무 10년 · 실전 진단 경험을 바탕으로 작성합니다.
이 글의 취약점, 내 사이트엔 없을까요? URL 하나로 30초 무료 점검.

RLS를 끈 Supabase는 공개 데이터베이스다

2025년에 AI 코딩 도구로 만든 170개 이상의 앱에서 사용자 데이터가 통째로 노출된 사건이 있었다. 공통점은 하나, Supabase RLS(Row Level Security)가 꺼져 있었다.

Supabase anon key는 클라이언트에 노출되는 게 의도된 설계다. 대신 RLS로 "이 key로는 이 데이터만 볼 수 있다"는 규칙을 정의해야 한다. RLS가 없으면 anon key 하나로 테이블 전체를 읽고, 쓰고, 삭제할 수 있다.

왜 위험한지 공격자 관점에서 보면 명확하다. Supabase는 모든 테이블에 대해 https://xxx.supabase.co/rest/v1/테이블명 형태의 REST API를 자동 생성한다. anon key는 브라우저 자바스크립트 번들이나 네트워크 요청 헤더에 그대로 들어 있어, 개발자도구 하나면 누구나 꺼낼 수 있다. RLS가 없다면 공격자는 그 key로 다음 한 줄이면 회원 테이블 전체를 덤프한다.

curl "https://xxx.supabase.co/rest/v1/users?select=*" \
  -H "apikey: eyJ...anon..." \
  -H "Authorization: Bearer eyJ...anon..."
# RLS가 없으면 → 전체 회원 이메일·전화번호·주소가 JSON으로 쏟아진다

레드팀 관점에서 신규 서비스를 진단할 때 가장 먼저 확인하는 것 중 하나가 이 Supabase 엔드포인트다. 응답이 200에 데이터가 그대로 나오면, 그 서비스는 사실상 로그인 없는 공개 DB를 운영하고 있는 셈이다. UI에 버튼이 없다고 데이터가 안전한 게 아니다, API는 UI를 거치지 않는다.

가장 흔한 실수 3가지

실수 1: RLS를 아예 활성화 안 함

Supabase 대시보드에서 테이블을 만들면 기본값이 RLS 비활성화다. AI가 생성한 코드도 대부분 RLS 설정을 포함하지 않는다. "일단 동작하니까" 넘어간 테이블 하나가 전체 서비스의 구멍이 된다.

-- RLS 활성화 (이것만 해도 일단 잠긴다)
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

RLS를 활성화하면 아무 정책도 없는 상태에서는 아무것도 읽을 수 없다(기본 deny). 이후 필요한 접근만 허용하는 정책을 추가한다. 순서가 중요하다, 먼저 전부 잠그고, 필요한 문만 하나씩 여는 방식이어야 실수가 사고로 이어지지 않는다.

직접 확인하는 법: SQL Editor에서 아래를 실행하면 RLS가 꺼진 테이블이 한눈에 잡힌다. relrowsecurityfalse인 테이블이 방치된 구멍이다.

SELECT relname AS table_name, relrowsecurity AS rls_on
FROM pg_class
WHERE relkind = 'r'
  AND relnamespace = 'public'::regnamespace
ORDER BY relrowsecurity;  -- false부터 정렬 → 위쪽이 위험 테이블

실수 2: 너무 느슨한 정책

RLS를 켰다고 끝이 아니다. 정책 조건이 헐거우면 켠 의미가 없다. 가장 흔한 패턴이 USING (true), "로그인만 했으면 다 통과"다. 이러면 A 사용자가 B 사용자의 주문 내역을 그대로 조회한다.

-- 위험: 로그인한 모든 사용자가 모든 행 열람 가능
CREATE POLICY "allow_all_authenticated"
ON users FOR SELECT
TO authenticated
USING (true);

-- 안전: 본인 데이터만
CREATE POLICY "own_data_only"
ON users FOR SELECT
USING ((select auth.uid()) = id);

auth.uid()(select auth.uid())로 감싼 이유가 있다. 그냥 auth.uid()를 쓰면 행마다 함수가 재평가돼 대규모 테이블에서 쿼리가 느려진다. 서브쿼리로 감싸면 플래너가 값을 한 번만 계산하고 캐싱해, 인덱스도 정상적으로 탄다. 성능과 보안을 동시에 챙기는 표준 패턴이다.

INSERT·UPDATE는 함정이 하나 더 있다. SELECT는 USING으로 검사하지만, 새로 들어오는 행은 WITH CHECK로 검사한다. WITH CHECK를 빼먹으면 사용자가 남의 user_id로 데이터를 심을 수 있다.

-- 안전: 본인 소유로만 쓰기 허용
CREATE POLICY "insert_own_only"
ON orders FOR INSERT
TO authenticated
WITH CHECK ((select auth.uid()) = user_id);

CREATE POLICY "update_own_only"
ON orders FOR UPDATE
TO authenticated
USING ((select auth.uid()) = user_id)       -- 어떤 행을 고를 수 있나
WITH CHECK ((select auth.uid()) = user_id);  -- 고친 뒤에도 여전히 내 것인가

실수 3: service_role key를 클라이언트에 노출

service_role key는 RLS를 완전히 무시한다. 위에서 정성껏 짠 정책이 이 key 앞에서는 전부 무력화된다. 절대 클라이언트 코드에 넣으면 안 된다. anon key만 클라이언트에 사용하고, service_role은 서버 사이드 전용으로 관리한다.

# .env.local (클라이언트용 — Next.js 등)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...  # anon key만

# .env (서버용)
SUPABASE_SERVICE_ROLE_KEY=eyJ...  # NEXT_PUBLIC_ 붙이지 않기

가장 흔한 유출 경로는 환경변수 접두사 실수다. Next.js에서 NEXT_PUBLIC_가 붙은 변수는 빌드 시 클라이언트 번들에 그대로 박힌다. service_role key에 실수로 이 접두사를 붙이면, 배포하는 순간 관리자 권한 key가 전 세계에 공개된다. Vite 계열은 VITE_, Create React App은 REACT_APP_가 같은 역할을 하니 프레임워크별 접두사 규칙을 반드시 확인한다.

직접 확인하는 법: 배포된 사이트에서 개발자도구 → Sources(또는 Network) 탭을 열고 Ctrl+Fservice_role을 검색한다. Supabase key는 JWT라, anon key를 jwt.io에 붙여넣어 payload의 "role" 값이 anon인지 확인하면 확실하다. service_role이 잡히면 즉시 대시보드에서 key를 rotate(재발급)하고, 유출된 기간의 접근 로그를 점검해야 한다.

RLS 제대로 설정된 것인지 검증하는 법

설정 후에는 반드시 다른 사용자 계정으로 로그인해서 남의 데이터가 보이는지 테스트해야 한다. 정책은 "짰다"가 아니라 "뚫어봤는데 안 뚫린다"까지 가야 검증이다. Supabase 대시보드의 "Auth > Users" 메뉴에서 테스트 계정을 만들고 SQL Editor에서 직접 쿼리해보는 게 가장 확실하다.

-- 다른 유저 ID로 테스트
SET LOCAL role TO authenticated;
SET LOCAL request.jwt.claims TO '{"sub": "other-user-uuid"}';
SELECT * FROM orders;  -- 빈 결과가 나와야 정상

세 각도에서 교차 검증하면 빠진 구멍이 드러난다.

  • anon 역할로 테스트, SET LOCAL role TO anon; 후 각 테이블을 조회한다. 로그인 없이 보이면 안 되는 데이터가 나오면 정책 누락이다.
  • 쓰기·수정·삭제까지, SELECT만 잠그고 INSERT/UPDATE/DELETE를 방치하는 경우가 많다. 남의 user_id로 INSERT가 성공하면 WITH CHECK가 빠진 것이다.
  • 실제 REST 엔드포인트로, 위의 curl 명령을 anon key로 직접 쏴본다. SQL Editor는 통과해도 PostgREST 경로에서 다르게 동작하는 엣지 케이스를 잡아낸다.

여기에 자주 놓치는 우회로가 하나 더 있다. SECURITY DEFINER로 만든 함수나 뷰는 RLS를 우회할 수 있다. 편의를 위해 만든 뷰 하나가 정책을 통째로 무력화하는 백도어가 되기도 하니, 함수·뷰의 권한 설정도 함께 점검한다.

바이브코딩 앱, 배포 전 30초 점검

Supabase RLS는 "켰다/껐다"의 문제가 아니라 테이블마다, 작업(읽기·쓰기·수정·삭제)마다 정책이 맞는지의 문제다. 테이블이 10개면 점검 포인트는 수십 개로 늘어난다. AI 코딩 도구로 빠르게 만든 서비스일수록 이 점검이 통째로 빠져 있는 경우가 많다.

내 서비스의 Supabase 엔드포인트가 외부에서 어떻게 보이는지, anon key로 데이터가 새는지, 노출된 key와 설정 오류가 없는지, CodeScan에 URL만 넣으면 21개 항목을 자동으로 점검한다. 개발자도구를 열기 전에, 먼저 밖에서 내 서비스가 어떻게 뚫리는지 확인하는 게 순서다.

지금 무료로 내 사이트 보안 점검하기 →

고객 데이터(CRM·회원 DB)를 다루는 서비스라면 CRM 보안 점검으로 개인정보 노출 위험까지 함께 확인하는 걸 권한다. 보안 사고는 터진 뒤엔 늦다. 배포 전 30초가 유출 대응 몇 주를 막는다.

Supabase RLS 데이터베이스 보안

내 사이트도 점검해보세요

URL 하나면 21개 항목을 30초에 무료 점검합니다. 회원가입·카드 없이.

추천 상품
웹서비스 런칭 전 보안 세팅
런칭 전에 반드시 해야 하는 보안 설정을 원격으로 직접 해드립니다. HTTPS, 환경변수 분리, 보안 헤더…
220,000원 150,000원

AI 코딩 보안 체크리스트 받기

AI 코딩 보안 체크리스트(PDF)를 무료로 받아보세요.