十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Tamagui Supabase 迁移规范:强制 RLS 行级安全的实战指南与事故复盘

Tamagui Supabase 迁移规范:强制 RLS 行级安全的实战指南与事故复盘 Tamagui Supabase 迁移规范强制 RLS 行级安全的实战指南与事故复盘【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui本文以 code/tamagui.dev/supabase/README.md 为骨架结合 tamagui.dev 站点的真实 Supabase 迁移脚本code/tamagui.dev/supabase/migrations与安全事故修复记录展开。你将掌握为什么 Supabase 中服务角色独享表也必须开启 RLS、如何为每种访问模式编写最小化 RLS 策略、如何用 SQL 审计零 RLS 漏洞以及如何避免 RLS 策略递归死循环等真实踩坑经验。核心原则publicschema 下每张表都必须启用 RLSSupabase 基于 PostgREST 将publicschema 下的每一张表都暴露为 REST API 端点并通过默认授权default grants将读写权限授予anon与authenticated两个角色。这意味着anon角色对应随客户端 bundle 一起分发的公开 anon key任何人拿到这个 key 就能直接通过 REST 调用你的 API禁用 RLS 并不会隐藏表——它恰恰让表对持有 anon key 的任何人完全可读、可写与你的应用代码内部如何读取毫无关系。文档README.md原文强调This table is only accessed via the service role, so it doesnt need RLS 是错误认知并记录了一起真实发生的提权事故——匿名用户通过直接 REST 调用把自己插入到pro_whitelist表从而自授予 Pro 权限。这一事故在迁移脚本中有完整佐证20260630000001_secure_users_and_theme_histories.sql 的头部注释写道root cause: several tables were created without enabling row level security (or with an over-broad policy), on the mistaken assumption that only the app accesses them via the service role is sufficient. it is not...为什么服务角色不能替代 RLS服务角色service role代码中对应supabaseAdmin客户端的权限是绕过 RLS的。它保护的是应用自己的代码路径却完全管不到直接打向 PostgREST 的 REST 调用。而 RLS 的作用面恰好是每个数据库行这一层无论请求是来自 PostgREST 还是其他 SQL 客户端只要角色不是绕过 RLS 的服务角色就会被策略约束。因此正确的思考方式是RLS 是唯一能约束持有 anon key 的任何人直连数据库的防线服务角色绕过 RLS恰恰说明服务角色独享表与开启 RLS二者不冲突——开启 RLS 但不添加任何策略服务角色照常工作而 anon / authenticated 得到默认拒绝。每张新表的四条强制规则按 README.md 的要求任何新建表都必须遵守无条件开启 RLSalter table public.t enable row level security;——没有例外仅服务角色访问如supabaseAdmin开启 RLS不添加任何策略。服务角色绕过 RLS 照常读写anon / authenticated 默认被拒绝需要用户级访问添加最窄的策略用auth.uid() owner col做行归属判断并且把cmd精确限定到具体操作for select、for update等不要图省事写for all刻意公开读如共享目录 / 公共数据只写for select to public using (true)永远不要写覆盖写操作的公共策略。禁止事项永远不要写to public with check (true)的 INSERT 策略——这等于允许 anon 匿名写入如果只有服务端插入数据就依赖服务角色不添加任何 INSERT 策略。迁移后必须执行的审计 SQL在添加迁移后文档要求验证线上真实状态与预期一致。下面这条查询会找出publicschema 下所有未开启 RLS 的表应返回零行select c.relname, c.relrowsecurity as rls from pg_class c join pg_namespace n on n.oid c.relnamespace where n.nspname public and c.relkind r and c.relrowsecurity false; -- ^ should return zero rows这段 SQL 是 RLS 合规的体检命令pg_class.relrowsecurity为false即表示该表未启用 RLS任何非零结果都是必须立即修复的合规缺口。建议将其作为每次部署前的固定检查项可放到 CI 或发布脚本中。仓库实战从无 RLS到策略化的迁移样本反面教材当初为什么出事20260202000001_create_pro_whitelist.sql 是事故表的原始创建脚本注释里明确写着-- NO RLS - this table is only accessed via supabaseAdmin (service role) -- this prevents any client-side manipulation正是这个仅服务角色访问、不需要 RLS的假设最终酿成了 anon 自助写入pro_whitelist的提权事故。这张表用于授予特定 GitHub 用户 Pro 权限github_username唯一索引 created_by记录管理员一旦被匿名写入攻击者即可给自己白名单化属于典型的高敏感表却长期裸奔。同样裸奔的还有public.theme_historiesanon 拥有全量 CRUD和public.team_subscriptions/public.team_members匿名可读写任意团队的席位与成员。修复迁移的正确姿势事故修复迁移 20260630000001_secure_users_and_theme_histories.sql 完整示范了开启 RLS 零策略/最小策略的组合拳-- 1) 用户表删除允许任意 authenticated 用户搜索他人的过宽策略 -- 该策略让每个人都能读到 email、billing_address、payment_method 等字段 drop policy if exists Allow users to search other users on public.users; -- 2) theme_histories启用 RLS只保留公开读主题通过 URL 共享 alter table public.theme_histories enable row level security; drop policy if exists Public can read shared themes on public.theme_histories; create policy Public can read shared themes on public.theme_histories for select to public using (true); -- 3) pro_whitelist启用 RLS不添加任何策略服务角色专属 alter table public.pro_whitelist enable row level security; -- 4) team_subscriptions team_members启用 RLS不添加任何策略 alter table public.team_subscriptions enable row level security; alter table public.team_members enable row level security; -- 5) 删除无角色限制、with check (true) 的冗余写策略 drop policy if exists Service role can insert domain history on public.project_domain_history;注意第 5 条原本名为Service role can insert domain history的策略没有角色限制默认to public且with check (true)导致 anon/authenticated 也能插入任意行。由于真实插入路径走服务角色绕过 RLS这个策略完全冗余直接删除即可。从公开读收紧到服务角色独享的第二阶段20260630000002_theme_histories_private_read.sql 展示了进一步收紧上一轮为兼容分享功能保留了public读策略但这仍让持有 anon key 的任何人能枚举所有用户的search_query_prompts theme_data user_id。因此第二阶段把读路径也全部迁到服务端 API 路由supabaseAdmin然后直接删除读策略使 RLS 处于已启用但零策略的完全锁定状态。该文件还特别提示了一个部署顺序问题NOTE: deploy ordering matters - the getTheme() change to read via the service role must be live BEFORE this policy is dropped, otherwise public /theme/ share links would break.即必须先上线读走服务角色的代码再删策略否则公开分享链接会先坏掉。这是 RLS 收紧迁移的标准节奏——先改代码路径后收策略。存储桶与函数级加固20260630000003_storage_upload_lockdown_and_search_path.sql 展示了 RLS 之外的配套加固-- 存储桶删除公开 INSERT 策略并限定 mime 类型与大小 drop policy if exists Theme Image Upload on storage.objects; update storage.buckets set allowed_mime_types array[image/png], file_size_limit 2097152 -- 2 MB where id theme-og-images;事故背景theme-og-images桶的 Theme Image Upload 策略是to public with check (bucket_id theme-og-images)无认证、无 mime/大小限制任何持有 anon key 的人都可向 tamagui 的 CDN 桶上传任意文件。修复后仅保留公开读OG 卡片仍需加载写入全部走服务角色。同一文件的另一个重点是给SECURITY DEFINER触发器函数固定search_pathalter function public.handle_new_user() set search_path ;这是防止搜索路径劫持的标准做法若不固定攻击者可以在路径前置位置植入同名对象来劫持函数内部的未限定引用。高级专题RLS 策略递归与 SECURITY DEFINER 破环20260630000004_fix_projects_rls_recursion.sql 记录了一个极易踩中的深坑——RLS 策略互相递归导致死循环public.projects的 Team members can view... SELECT 策略内部执行EXISTS(... from project_team_members ...)public.project_team_members的 Project owners can manage... ALL 策略内部执行EXISTS(... from projects ...)于是评估任一张表的策略都会触发另一张表的策略无限循环表现为authenticated角色直接 SELECTpublic.projects时报错infinite recursion detected in policy for relation projects。解决方案是把跨表检查抽成SECURITY DEFINER辅助函数——函数以属主身份执行、绕过被查询表的 RLS从而切断循环同时用set search_path 固定搜索路径防止被劫持create or replace function public.is_project_owner(p_project_id uuid, p_user_id uuid) returns boolean language sql security definer set search_path stable as $$ select exists ( select 1 from public.projects where id p_project_id and user_id p_user_id ); $$;策略改为调用函数create policy Team members can view projects they belong to on public.projects for select to public using (public.is_project_member(id, (select auth.uid())));这里还有一层重要的失败关闭fails closed观察该站点应用通过服务角色读取这两张表绕过 RLS因此递归问题在线上是潜伏的直到有人用 authenticated 角色直连才暴露。RLS 配置错误不一定会立刻炸在你脸上这正是必须用审计 SQL 主动验证的原因。对照样本用户级策略与索引设计不是所有表都该零策略。需要用户自管理的表应使用最窄的归属策略仓库中有两个早期迁移可作对照20240629220913_add_rls_to_users_private.sql 为users_private表只开了用户更新自己的行create policy Allow users to update their own row on public.users_private as permissive for update to public using (auth.uid() id); alter table public.users_private enable row level security;注意顺序先建策略、后开启 RLS避免开启瞬间出现策略空缺窗口。20230731081829_add_rls_to_app_installations.sql 则示范了基于关联表存在性的读策略create policy Enable users to see their own app installations on public.app_installations as permissive for select to public using ((EXISTS ( SELECT 1 FROM subscription_items WHERE (subscription_items.id app_installations.subscription_item_id))));同样地pro_whitelist表还配套了索引以支撑访问检查20260202000001_create_pro_whitelist.sql 中pro_whitelist_github_username_idx用于按 GitHub 用户名快速查找。服务角色的代码侧印证所有合法应用访问都走服务角色并非口头承诺在站点代码中可大量验证supabaseAdmin服务角色客户端被广泛用于订阅、Bento 下载、Discord、GitHub bot 等 API 路由例如 add-team-seatsapi.ts、admin/whitelistapi.ts、admin/impersonateapi.ts 等。这正是文档所述安全模型的落点应用逻辑全部服务端化客户端只接触受限的 RLS 面。另外code/tamagui.dev/supabase/config.toml 中[api]段的max_rows 1000也值得注意——这是对意外或恶意请求的又一层纵深防御防止单次 REST 调用拉走全表数据。可落地的检查清单综合文档与仓库实践为你的下一个 Supabase 迁移整理如下清单新表第一行就写alter table public.t enable row level security;没有例外按访问模型选择策略形态服务角色专属 → 零策略用户自管 →auth.uid() owner且限定具体for cmd公开读 → 仅for select to public using (true)迁移后运行文档的审计 SQL确认public下无任何relrowsecurity false的表收紧既有策略时遵循先改代码路径服务角色后删策略的部署顺序避免线上功能中断跨表引用的 RLS 策略优先抽成SECURITY DEFINER函数并set search_path 同时留意递归死循环的报错特征存储桶策略与SECURITY DEFINER函数的search_path一并纳入安全检查范围不给影子对象劫持留机会把审计 SQL 接入 CI/发布流程让RLS 缺口在合并前而非事故后被发现。记住那句被事故验证过的判断仅服务角色访问所以不需要 RLS 是 Supabase 安全模型中最危险的假设。RLS 是唯一能把 anon key 挡在行数据之外的手段而它只需要一行alter table。【免费下载链接】tamaguiStyle React fast with 100% parity on React Native, an optional UI kit, and optimizing compiler.项目地址: https://gitcode.com/GitHub_Trending/ta/tamagui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表