보안 가이드 2026.02.18 · 조회 19796

Supabase RLS 설정 실수 방지법

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

S
보안 전문 기업 SENTRIX 레드팀
해킹대회 수상 · 보안 실무 10년 · 실전 진단 경험을 바탕으로 작성합니다.
웹사이트를 운영하고 있다면
보안 헤더·인증서·공개 노출부터 무료로 점검하세요. 결과 링크는 이메일로 받습니다. 로그인 이후 권한·업무 로직은 별도 검증이 필요합니다.

다음 화면에서 이메일과 점검 권한을 확인합니다. 회원가입·카드 없이 무료 요약을 받고, 상세 리포트는 선택 구매합니다.

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가 꺼진 테이블이 한눈에 잡힌다. relrowsecurity가 false인 테이블이 방치된 구멍이다.

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+F로 service_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초가 유출 대응 몇 주를 막는다.

내 사이트도 점검해보세요

URL 입력, 결과 링크는 메일로 받는 무료 점검. 회원가입·카드 불필요.

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

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

배포·설정을 바꾼 뒤에는 같은 주소를 다시 점검하세요. 수정 후 재점검 순서와 일정 저장 · 자동 점검이 필요하면 Continuous의 범위와 체험 조건을 확인하세요.
이런 글을 계속 받아보려면 뉴스레터 구독 · RSS 피드