● CWE-862 · OWASP A01:2021

Supabase와 Firebase 접근 규칙(CWE-862): Row Level Security 누락이 데이터를 노출하는 방식과 해결 방법

Supabase와 Firebase 앱은 브라우저가 공개 키로 데이터베이스에 직접 쿼리하게 하므로, 방문자와 모든 행 사이에 있는 것은 Row Level Security나 Security Rules뿐입니다. enable row level security 없이 SQL로 만든 테이블이나 allow read, write: if true로 남겨 둔 Firestore 규칙은 누구나 읽고 쓸 수 있습니다. USING과 WITH CHECK 정책이 행을 로그인한 사용자로 어떻게 한정하는지 PostgreSQL 마이그레이션 예제로 설명합니다.

알기 쉬운 설명 (ELI5)

어느 사무실 건물은 모든 방문객에게 안내 책자 뒷면에 인쇄된 똑같은 로비 출입 카드를 줍니다. 층마다 문이 열리기 전에 신원을 확인하는 한 문제는 없습니다. 그런데 서둘러 꾸민 한 층의 문에는 카드 리더가 아예 없어서 로비 카드로 열리고, 누구든 들어가 서류를 읽을 수 있습니다. Supabase의 anon 키가 바로 이 로비 카드이며, 원래 공개되는 것입니다. Row Level Security는 층마다 문에 달린 카드 리더입니다. 해결책은 모든 문에 리더를 달고, 각 사람이 누구의 서류를 볼 수 있는지 정확히 알려 주는 것입니다.

핵심 개념 및 용어

공개 anon 키
Supabase의 anon 키와 Firebase 웹 설정은 앱 번들 안에 들어 있어 누구나 복사할 수 있습니다. 이들은 사용자가 아니라 프로젝트를 식별하므로 비밀이 아니며, 그 자체로는 데이터를 보호하지 못합니다.
Row Level Security(RLS)
모든 쿼리를 정책으로 걸러 내는 PostgreSQL 기능입니다. Supabase의 Table Editor는 새 테이블에 이를 켜 주지만, SQL이나 마이그레이션으로 만든 테이블은 RLS가 꺼진 채로 시작하고, 기본 권한 때문에 anon과 authenticated가 모든 행을 읽고 바꿀 수 있습니다.
USING과 WITH CHECK
USING은 사용자가 볼 수 있거나 수정·삭제할 수 있는 기존 행을 정하고, WITH CHECK는 쓸 수 있는 새 행이나 바뀐 행을 정합니다. insert나 update 정책에 USING만 있으면 다른 조직의 행을 쓸 수 있습니다.
service_role 키
service_role 키는 RLS를 완전히 우회합니다. 서버와 엣지 함수에서만 써야 하며, 브라우저나 모바일 앱에는 절대 넣지 않습니다.
Firestore 테스트 모드 규칙
테스트 모드로 시작한 프로젝트는 특정 날짜까지 모든 읽기와 쓰기를 허용하는 규칙을 받으며, 이후 allow read, write: if true로 바뀌는 경우가 많습니다. 규칙은 request.auth.uid를 문서의 소유자 필드와 비교해야 합니다.

단계별 공격 실행 흐름

단계 1

공격자가 앱을 엶

Supabase URL과 anon 키가 JavaScript 번들이나 모바일 앱 패키지에 들어 있습니다.

단계 2

테이블을 직접 쿼리함

앱 화면을 거치지 않고 anon 키만으로 invoices의 REST 엔드포인트를 호출합니다. anon 키만 쓰고 사용자 세션 없이 테스트 프로젝트에 보내는 요청이 이 경우를 보여 줍니다.

단계 3

데이터베이스가 모든 행을 반환함

RLS가 꺼져 있고 anon에 select 권한이 있으므로 PostgreSQL은 아무것도 거르지 않고 모든 조직의 청구서를 돌려줍니다.

단계 4

데이터가 유출되거나 바뀜

update와 delete까지 허용되어 있다면 공격자는 레코드를 바꾸거나 지울 수도 있습니다. 2025년 연구자들이 한 AI 앱 빌더가 생성한 170개 이상의 앱에서 RLS 누락으로 데이터가 노출된 것을 발견해 CVE-2025-48757이 부여되었습니다.

소스 코드 비교: 취약한 구현 vs 보안 패치

취약한 구현
-- migration.sql: 이 테이블에는 RLS가 한 번도 켜지지 않는다
create table public.invoices (
  id bigint generated always as identity primary key,
  org_id uuid not null references public.orgs (id),
  customer text not null,
  amount_cents bigint not null
);

-- 공개 anon 키로 모든 행을 읽고 바꾸고 지울 수 있다
grant select, insert, update, delete on public.invoices to anon, authenticated;
보안 강화 패치
-- migration.sql: RLS 켜기, anon 권한 회수, 명령마다 정책 하나
create table public.invoices (
  id bigint generated always as identity primary key,
  org_id uuid not null references public.orgs (id),
  customer text not null,
  amount_cents bigint not null
);

-- RLS가 켜지면 정책이 없는 작업은 모두 거부된다
alter table public.invoices enable row level security;
revoke all on public.invoices from anon;

-- 사용자는 자신이 속한 조직의 청구서만 본다
create policy "members read their org's invoices"
  on public.invoices for select to authenticated
  using (org_id in (select org_id from public.memberships where user_id = (select auth.uid())));

-- 새 행은 사용자 조직의 것이어야 한다; update·delete 정책은 없다
create policy "members add invoices to their org"
  on public.invoices for insert to authenticated
  with check (org_id in (select org_id from public.memberships where user_id = (select auth.uid())));

엔지니어링 및 시스템 보안 강화 체크리스트

출처

← 전체 보안 디렉터리 보기 모든 취약점 가이드 →