Правила доступа в Supabase и Firebase (CWE-862): как отсутствие Row Level Security раскрывает данные и как это исправить
Приложения на Supabase и Firebase позволяют браузеру обращаться к базе данных напрямую с публичным ключом, поэтому между посетителем и каждой строкой стоят только Row Level Security или Security Rules. Таблицу, созданную через SQL без enable row level security, или правило Firestore, оставленное в виде allow read, write: if true, может читать и менять кто угодно. Как политики USING и WITH CHECK ограничивают строки вошедшим пользователем, на примере миграции PostgreSQL.
Простыми словами (ELI5)
Офисное здание выдаёт каждому посетителю одну и ту же карту для входа в холл, напечатанную на обороте буклета. Это не страшно, пока дверь каждого этажа проверяет, кто вы, прежде чем открыться. Один этаж оборудовали впопыхах, и на его двери вообще нет считывателя, поэтому карта холла её открывает, и любой может войти и читать документы. Ключ anon в Supabase — это карта холла: он задуман публичным. Row Level Security — это считыватель на двери каждого этажа. Исправление — поставить считыватель на каждую дверь и точно указать ему, чьи документы может видеть каждый человек.
Ключевые понятия и термины
- Публичный ключ anon
- Ключ
anonSupabase и веб-конфигурация Firebase поставляются внутри пакета приложения, и любой может их скопировать. Они определяют проект, а не пользователя, поэтому не являются секретом и сами по себе данные не защищают. - Row Level Security (RLS)
- Функция PostgreSQL, которая пропускает каждый запрос через политики. Table Editor в Supabase включает её для новых таблиц, но таблицы, созданные через SQL или миграции, начинают с выключенной RLS, и права по умолчанию позволяют
anonиauthenticatedчитать и менять любую строку. - USING и WITH CHECK
USINGопределяет, какие существующие строки пользователь может видеть, изменять или удалять;WITH CHECK— какие новые или изменённые строки он может записать. Политика только сUSINGдля insert или update позволяет записывать строки для чужой организации.- Ключ service_role
- Ключ
service_roleполностью обходит RLS. Ему место только на серверах и в edge-функциях, но никогда не в браузере или мобильном приложении. - Правила Firestore в тестовом режиме
- Проекты, запущенные в тестовом режиме, получают правила, разрешающие любое чтение и запись до определённой даты, и многие затем превращают их в
allow read, write: if true. Правила должны сверятьrequest.auth.uidс полем владельца документа.
Пошаговый сценарий атаки
Атакующий открывает приложение
URL Supabase и ключ anon лежат в JavaScript-бандле или пакете мобильного приложения.
Он обращается к таблице напрямую
Он вызывает REST-эндпоинт invoices только с ключом anon, минуя экраны приложения. Запрос к тестовому проекту с ключом anon и без пользовательской сессии показывает этот случай.
База возвращает все строки
RLS выключена, а у anon есть право select, поэтому PostgreSQL ничего не фильтрует и отдаёт счета всех организаций.
Данные утекают или меняются
Если выданы ещё и update и delete, атакующий может изменять или стирать записи. В 2025 году был присвоен CVE-2025-48757 после того, как исследователи обнаружили, что отсутствие RLS раскрывало данные более чем в 170 приложениях, созданных одним ИИ-конструктором приложений.
Исходный код: Уязвимый 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())));
Чек-лист по защите системы для инженеров
- Включите RLS на каждой таблице в открытых схемах и проваливайте CI, если
select tablename from pg_tables where schemaname = 'public' and not rowsecurityвозвращает хотя бы одну строку. - Пишите по одной политике на команду с ограничением
to authenticated:USINGдля чтения иWITH CHECKдля insert и update; не добавляйте ненужные политики update и delete. - Отзовите права на таблицу у
anon, если анонимным пользователям незачем её читать, и храните ключservice_roleтолько в серверных окружениях. - Включите RLS и на таблицах, которые читают ваши политики, например
memberships, чтобы пользователи не могли добавить себя в чужую организацию. - Проверяйте на двух тестовых пользователях: пользователь A должен получать ноль строк по организации пользователя B; для Firestore запускайте модульные тесты правил в эмуляторе через
@firebase/rules-unit-testing.