Supabase/Firebase를 쓴다면 꼭 봐야 할 권한 설정 문제

카페인코더·2026년 6월 15일

AI 웹앱 보안 점검

목록 보기
3/11
post-thumbnail

지난 글에서는 웹앱 배포 후 외부에서 확인할 수 있는 보안 점검 항목들을 정리했다.

이번 글에서는 Supabase나 Firebase처럼 클라이언트에서 직접 DB/API를 사용하는 구조에서 특히 조심해야 할 권한 설정 문제를 정리해보려고 한다.

먼저 Supabase와 Firebase가 무엇인지부터 간단히 짚고 넘어가자.


Supabase와 Firebase는 무엇인가

웹앱을 만들다 보면 보통 이런 기능들이 필요하다.

  • 회원가입/로그인
  • 데이터베이스
  • 파일 업로드
  • 실시간 데이터
  • 서버리스 함수
  • 인증/권한 처리

예전에는 이런 기능을 직접 백엔드 서버에서 많이 구현했다.

브라우저 → 내 백엔드 서버 → DB

이 구조에서는 보통 백엔드 서버가 중간에서 사용자의 요청을 검사한다.

예를 들어 사용자가 /api/orders/123을 요청하면 서버가 확인한다.

이 사용자가 123번 주문을 볼 수 있는 사람인가?

그런데 Supabase나 Firebase를 쓰면 구조가 조금 달라질 수 있다.

브라우저 → Supabase/Firebase → DB 또는 Storage

프론트엔드 코드에서 직접 Supabase나 Firebase SDK를 호출해서 데이터를 읽고 쓰는 방식이다.

이 방식은 정말 편하다.
AI 개발 도구와도 잘 맞는다.
로그인, DB, 파일 업로드까지 빠르게 붙일 수 있다.

하지만 편한 만큼 중요한 질문이 생긴다.

누가 어떤 데이터에 접근할 수 있는지는 어디서 막고 있는가?

이 질문에 제대로 답하지 못하면, 기능은 잘 동작하지만 권한은 뚫려 있는 서비스가 될 수 있다.


로그인과 권한은 다르다

가장 먼저 구분해야 할 것은 인증(Authentication)인가/권한(Authorization) 이다.

인증은 “누구인지 확인하는 것”이다.

이 사용자가 로그인한 사용자인가?

권한은 “무엇을 할 수 있는지 확인하는 것”이다.

이 사용자가 이 데이터를 볼 수 있는가?
이 사용자가 이 주문을 수정할 수 있는가?
이 사용자가 관리자 기능을 실행할 수 있는가?

로그인이 되어 있다고 해서 모든 데이터에 접근할 수 있어야 하는 것은 아니다.

예를 들어 A 사용자가 로그인했다고 해서 B 사용자의 주문 내역을 볼 수 있으면 안 된다.

로그인 여부 확인: O
소유자/권한 확인: X

이 상태가 꽤 위험하다.

“로그인한 사용자만 접근 가능”은 충분한 권한 설계가 아니다.


Supabase에서 조심해야 할 것: RLS

Supabase는 PostgreSQL 기반의 백엔드 플랫폼이다.
DB, Auth, Storage, Edge Functions 등을 제공하고, 클라이언트에서 Supabase API를 직접 호출할 수 있다.

Supabase에서 권한 설정의 핵심은 RLS(Row Level Security) 다.

RLS는 말 그대로 “행 단위 보안”이다.
테이블 안의 각 row에 대해, 어떤 사용자가 읽고 쓸 수 있는지 정책을 정하는 방식이다.

예를 들어 todos 테이블이 있다고 하자.

id | user_id | title

이때 사용자는 자기 user_id에 해당하는 todo만 볼 수 있어야 한다.

Supabase에서는 이런 정책을 작성할 수 있다.

create policy "Users can view their own todos"
on todos
for select
to authenticated
using ((select auth.uid()) = user_id);

의미는 대략 이렇다.

로그인한 사용자만 select 가능하다.
단, auth.uid()가 row의 user_id와 같을 때만 가능하다.

즉 단순히 “로그인했는가”가 아니라, 이 row의 소유자인가를 확인한다.

Supabase 공식 문서에서도 브라우저에서 안전하게 데이터에 접근하려면 RLS를 활성화해야 한다고 설명한다.
특히 외부에 노출된 schema의 테이블에는 RLS를 항상 활성화해야 한다고 안내한다.


Supabase에서 흔한 위험 패턴

1. RLS가 꺼져 있는 테이블

Supabase Table Editor로 만든 테이블은 RLS가 기본 활성화될 수 있지만, SQL Editor나 raw SQL로 만든 테이블은 직접 확인해야 한다.

위험한 상태는 이런 것이다.

테이블은 public schema에 있음
클라이언트에서 접근 가능함
그런데 RLS가 꺼져 있음

이 경우 권한 정책이 제대로 적용되지 않을 수 있다.

배포 전에 최소한 이런 질문을 해야 한다.

사용자 데이터가 들어가는 모든 테이블에 RLS가 켜져 있는가?

2. using (true) 정책을 대충 넣어둔 경우

개발 중에는 빨리 테스트하려고 이런 식의 정책을 넣을 수 있다.

using (true)

이 정책은 말 그대로 조건이 항상 참이다.

물론 공개 프로필이나 공개 게시글처럼 모두가 읽어도 되는 데이터에는 의도적으로 사용할 수 있다.

하지만 사용자별 개인 데이터, 주문, 문의, 예약, 결제 정보에 이런 정책이 들어가 있으면 위험하다.

이 데이터는 정말 모두가 읽어도 되는가?

이 질문에 바로 답하지 못하면 using (true)는 의심해봐야 한다.


3. authenticated면 모두 허용하는 경우

이런 정책도 조심해야 한다.

to authenticated
using (true)

이건 “로그인한 사용자라면 모두 허용”에 가깝다.

게시글 목록처럼 로그인한 사용자 전체가 봐도 되는 데이터라면 괜찮을 수 있다.
하지만 개인 데이터라면 부족하다.

예를 들어 다음 데이터에는 보통 소유자 조건이 필요하다.

  • 내 주문
  • 내 예약
  • 내 문의
  • 내 결제 내역
  • 내 프로필
  • 내 업로드 파일
  • 내 프로젝트

이런 데이터는 보통 아래처럼 사용자 식별자와 연결되어야 한다.

using ((select auth.uid()) = user_id)

핵심은 이것이다.

로그인 여부가 아니라, row의 소유자인지를 확인해야 한다.


4. service_role key를 클라이언트에 넣는 경우

Supabase에는 클라이언트에서 써도 되는 publishable key 또는 legacy anon key가 있다.
이 키들은 공개 환경에서 쓰는 것을 전제로 한다.

반대로 secret key나 legacy service_role key는 서버에서만 써야 한다.

특히 service_role은 RLS를 우회할 수 있는 강한 권한을 가진다.

anon / publishable key: 클라이언트 사용 가능
service_role / secret key: 서버 전용

프론트엔드 코드, .env의 public 환경변수, GitHub 저장소, 브라우저 번들 안에 service_role key가 들어가면 안 된다.

Next.js라면 NEXT_PUBLIC_에 넣으면 브라우저로 노출될 수 있다.
Vite라면 VITE_에 넣으면 클라이언트 번들에 포함될 수 있다.

service_role key는 브라우저에서 쓰는 키가 아니다.


Firebase에서 조심해야 할 것: Security Rules

Firebase는 Google의 앱 개발 플랫폼이다.
Authentication, Firestore, Realtime Database, Storage, Hosting, Cloud Functions 등을 제공한다.

Firebase에서 클라이언트가 Firestore나 Storage에 직접 접근하는 경우, 핵심은 Firebase Security Rules 다.

Security Rules는 클라이언트 요청이 데이터에 접근해도 되는지 판단하는 규칙이다.

Firestore 예시는 이런 형태다.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, update, delete: if request.auth != null
                                  && request.auth.uid == userId;
      allow create: if request.auth != null;
    }
  }
}

여기서 중요한 부분은 이 조건이다.

request.auth.uid == userId

로그인한 사용자이면서, 요청한 문서의 userId와 본인의 uid가 같을 때만 허용한다.


Firebase에서 흔한 위험 패턴

1. 개발용 open rule을 그대로 배포하는 경우

가장 위험한 패턴은 이런 규칙이다.

allow read, write: if true;

이건 사실상 누구나 읽고 쓸 수 있는 상태다.

개발 중에는 편할 수 있지만, 운영 환경에서는 매우 위험하다.

Firebase 공식 문서에서도 개발 중 open access로 설정한 규칙을 그대로 배포하면, 프로젝트 ID를 아는 사람이 데이터를 훔치거나 수정하거나 삭제할 수 있다고 경고한다.


2. 로그인만 확인하는 경우

이런 규칙도 조심해야 한다.

allow read, write: if request.auth != null;

이 규칙은 “로그인한 사용자면 가능”이라는 뜻이다.

문제는 모든 로그인 사용자가 모든 데이터를 읽거나 쓸 수 있는 구조가 될 수 있다는 점이다.

예를 들어 /orders/{orderId}에 이런 규칙이 있다면, A 사용자가 B 사용자의 주문도 볼 수 있는지 확인해야 한다.

대부분의 개인 데이터에는 이런 조건이 필요하다.

request.auth.uid == resource.data.userId

또는 경로 자체를 사용자 기준으로 설계할 수도 있다.

match /users/{userId}/orders/{orderId} {
  allow read, write: if request.auth != null
                     && request.auth.uid == userId;
}

3. 클라이언트가 보내는 role 값을 믿는 경우

관리자 기능을 만들 때 이런 실수를 할 수 있다.

클라이언트가 role: "admin"을 보내면 관리자 처리

이건 위험하다.

클라이언트에서 보내는 값은 사용자가 조작할 수 있다고 봐야 한다.
관리자 여부는 서버에서 검증하거나, Firebase Custom Claims, 별도 관리자 문서, Cloud Functions 같은 구조로 신중하게 다뤄야 한다.

프론트엔드에서 버튼을 숨기는 것 ≠ 관리자 권한 보호

버튼이 안 보여도 API나 DB 요청은 직접 보낼 수 있기 때문이다.


4. Rules를 필터처럼 착각하는 경우

Firestore Security Rules에서 중요한 개념 중 하나는 Rules are not filters다.

보안 규칙은 “허용된 데이터만 알아서 걸러서 반환하는 필터”가 아니다.

쿼리 자체가 규칙을 만족할 수 있어야 한다.

예를 들어 공개 문서만 읽을 수 있는 규칙이 있다면, 클라이언트 쿼리도 공개 문서만 요청하도록 작성되어야 한다.

// 예시
where("visibility", "==", "public")

규칙이 알아서 전체 컬렉션 중 허용된 문서만 골라주는 방식으로 기대하면 안 된다.


파일 업로드도 권한이 필요하다

Supabase Storage나 Firebase Storage를 쓰면 이미지, 첨부파일, 프로필 사진 같은 기능을 빠르게 만들 수 있다.

하지만 파일도 데이터다.

확인해야 할 질문은 다음과 같다.

  • 누구나 업로드할 수 있는가?
  • 로그인한 사용자만 업로드할 수 있는가?
  • 사용자가 자기 폴더에만 업로드할 수 있는가?
  • 다른 사용자의 파일을 읽을 수 있는가?
  • public bucket이어도 괜찮은 파일인가?
  • 파일 URL만 알면 누구나 볼 수 있는가?

프로필 이미지처럼 공개되어도 되는 파일과, 계약서/증빙자료/상담 첨부파일처럼 공개되면 안 되는 파일은 다르게 다뤄야 한다.

Storage를 public으로 열기 전에, 그 파일이 정말 공개 파일인지 확인해야 한다.


배포 전에 확인할 체크리스트

Supabase나 Firebase를 쓰는 프로젝트라면 배포 전에 최소한 아래를 확인하는 것이 좋다.

Supabase

  • 사용자 데이터가 들어가는 테이블에 RLS가 켜져 있는가?
  • using (true) 정책이 민감 데이터에 적용되어 있지 않은가?
  • authenticated 전체 허용 정책이 과하게 넓지 않은가?
  • auth.uid() = user_id 같은 소유자 조건이 필요한 곳에 들어가 있는가?
  • insert/update 정책에 with check가 필요한지 확인했는가?
  • service_role key가 클라이언트 코드에 들어가 있지 않은가?
  • Storage bucket이 public이어도 되는지 확인했는가?
  • Storage object에도 필요한 RLS 정책이 있는가?

Firebase

  • allow read, write: if true가 운영 규칙에 남아 있지 않은가?
  • request.auth != null만으로 모든 데이터를 허용하고 있지 않은가?
  • 사용자별 데이터에 request.auth.uid == userId 같은 조건이 있는가?
  • 관리자 권한을 클라이언트 값만 믿고 있지 않은가?
  • Firestore Rules와 Storage Rules를 각각 확인했는가?
  • Rules Simulator나 Emulator로 비로그인/다른 사용자 접근을 테스트했는가?
  • Firebase API key를 비밀키처럼 오해하지 않고, Security Rules/App Check/IAM 관점에서 보고 있는가?

AI에게 점검을 시킬 때 쓸 수 있는 프롬프트

Codex, Claude, Cursor처럼 프로젝트 저장소를 읽을 수 있는 AI를 쓰고 있다면 아래처럼 물어볼 수 있다.

이 프로젝트가 Supabase 또는 Firebase를 사용하고 있는지 확인해줘.

사용하고 있다면 배포 전 권한 설정 관점에서 아래 항목을 점검해줘.

1. Supabase 사용 시
- RLS가 필요한 테이블을 추정해줘.
- 사용자별 데이터인데 RLS 정책이 없거나 과하게 넓은 곳이 있는지 봐줘.
- service_role 또는 secret key가 클라이언트 코드에 노출될 가능성이 있는지 확인해줘.
- Storage bucket이나 파일 접근 권한에서 위험 후보가 있는지 봐줘.

2. Firebase 사용 시
- Firestore/Realtime Database/Storage Security Rules 파일을 찾아줘.
- allow read, write: if true 같은 개발용 규칙이 남아 있는지 확인해줘.
- request.auth != null만으로 과하게 넓게 허용하는 규칙이 있는지 봐줘.
- 사용자별 데이터에 request.auth.uid 기반 조건이 있는지 확인해줘.
- 관리자 권한을 클라이언트 값만 믿는 코드가 있는지 찾아줘.

출력은 다음 형식으로 해줘.

- 위험도: 높음 / 보통 / 낮음
- 위치: 파일명과 코드 위치
- 이유:
- 확인 방법:
- 수정 방향:

실제 API key, 비밀번호, 토큰 값은 출력하지 말고 존재 여부와 위치만 알려줘.
확실하지 않은 내용은 취약점이라고 단정하지 말고 "위험 후보"라고 표현해줘.

이 프롬프트가 전문 보안 진단을 대신할 수는 없다.
그래도 배포 전 기본적인 권한 실수를 찾는 데는 도움이 될 수 있다.


마무리

Supabase와 Firebase는 빠르게 웹앱을 만들 수 있게 해준다.

특히 AI 개발 도구와 함께 쓰면 로그인, DB, 파일 업로드까지 정말 빠르게 붙일 수 있다.

하지만 구조가 편해질수록 권한 경계는 더 분명하게 봐야 한다.

전통적인 백엔드 구조에서는 서버 코드가 권한을 막아주는 경우가 많았다.
반면 Supabase/Firebase처럼 클라이언트가 직접 DB/API에 접근하는 구조에서는 RLS나 Security Rules가 중요한 방어선이 된다.

핵심은 이것이다.

로그인 기능을 붙였다고 권한 처리가 끝난 것은 아니다.

배포 전에 최소한 아래 질문은 해봐야 한다.

A 사용자가 B 사용자의 데이터를 볼 수 없는가?
A 사용자가 B 사용자의 데이터를 수정할 수 없는가?
일반 사용자가 관리자 기능을 실행할 수 없는가?
공개되면 안 되는 파일이 public으로 열려 있지 않은가?
서버 전용 key가 클라이언트에 들어가 있지 않은가?

AI로 만든 웹앱일수록 기능이 동작하는지만 보지 말고, 데이터가 누구에게 보이는지도 같이 확인해야 한다.


참고자료

profile
꾸준히 기록 중 입니다.

0개의 댓글