关于 supabase-security-basics
Supabase Security Basics 是帮助开发者保护 Supabase 项目的技能,涵盖 API 密钥隔离、RLS 策略和 API 加固,并提供 SQL 与 JavaScript 示例。
主要功能
- 01区分客户端与服务端密钥
- 02RLS 策略模式与审计
- 03API 加固清单
- 04SQL 注入防护指导
- 05安全错误诊断
适用场景
- 01审计 public 表的 RLS 覆盖情况
- 02将 service role 密钥限制在服务端代码
- 03加固 CORS 和数据库连接
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jeremylongshore/ tons-of-skills-marketplace --skill supabase-security-basics安装后在 Claude Code 中输入/supabase-security-basics确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/errors.mdexamples.mdimplementation.md详细介绍
Supabase 安全基础
概述
Supabase 通过 PostgREST 将 Postgres 数据库直接暴露到互联网。任何未启用行级安全(RLS)的表,只要有人拥有你的项目 URL 和 anon key(两者都是公开的),就可以被其完全读取和写入。本技能介绍 Supabase 安全的三大支柱:密钥分离(anon 与 service_role)、RLS 策略强制执行,以及 API 面加固。
前提条件
- 已创建 Supabase 项目(本地或托管),并可访问 Dashboard
- 已安装
@supabase/supabase-js(npm install @supabase/supabase-js) - 已配置
SUPABASE_URL和SUPABASE_ANON_KEY环境变量 - 了解 SQL 和 Postgres 基础知识
说明
步骤 1 — 了解两种 API 密钥
Supabase 为每个项目提供两种密钥。混淆它们是最常见的安全错误:
| 密钥 | 环境变量 | 是否暴露给客户端? | RLS 行为 |
|---|---|---|---|
| Anon key | SUPABASE_ANON_KEY | 是 — 可安全用于浏览器 | 遵循所有 RLS 策略 |
| Service role key | SUPABASE_SERVICE_ROLE_KEY | 绝不可暴露 | 绕过所有 RLS |
anon key 是一个 JWT,PostgREST 会用它来确定适用哪些 RLS 策略。将它包含在客户端代码包中是安全的——它只能访问 RLS 策略明确允许的数据。service role key 会绕过所有 RLS 策略,因此只能存在于服务端代码中(API 路由、Edge Functions、定时任务、迁移脚本)。
// CORRECT: anon key on the client
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// CORRECT: service role key ONLY in server-side code
// e.g., app/api/admin/route.ts (Next.js server route)
import { createClient } from '@supabase/supabase-js'
const supabaseAdmin = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
)
// WRONG — service role key in client-side code
// This bypasses ALL RLS and leaks your admin key to every user
const supabase = createClient(url, process.env.NEXT_PUBLIC_SERVICE_ROLE_KEY!) // NEVER DO THIS
**密钥轮换:**在 Dashboard > Settings > API 中重新生成密钥。轮换后,更新所有环境变量并重新部署所有服务。旧密钥会立即失效——没有宽限期。
步骤 2 — 为每张表强制启用行级安全
如果未启用 RLS,public schema 中的任何表都可通过 REST API 被持有 anon key 的任何人完全访问。RLS 不是可选项——它是主要的访问控制层。
-- Audit: find tables missing RLS
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- Enable RLS on every public table
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.todos ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- CRITICAL: enabling RLS with NO policies blocks ALL access via the API.
-- You MUST add at least one policy per table per operation (SELECT, INSERT, UPDATE, DELETE).
策略模式 — 用户读取和写入自己的行:
-- SELECT: user can only read their own rows
CREATE POLICY "Users read own data"
ON public.todos FOR SELECT
USING (auth.uid() = user_id);
-- INSERT: user can only insert rows for themselves
CREATE POLICY "Users insert own data"
ON public.todos FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- UPDATE: user can only update their own rows
CREATE POLICY "Users update own data"
ON public.todos FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- DELETE: user can only delete their own rows
CREATE POLICY "Users delete own data"
ON public.todos FOR DELETE
USING (auth.uid() = user_id);
策略模式 — 公开读取、已认证用户写入:
CREATE POLICY "Anyone can read posts"
ON public.posts FOR SELECT
USING (true);
CREATE POLICY "Authenticated users can insert"
ON public.posts FOR INSERT
WITH CHECK (auth.uid() IS NOT NULL);
策略模式 — 通过自定义 JWT 声明进行基于角色的访问控制:
-- Admin-only policy using app_metadata
CREATE POLICY "Admins have full access"
ON public.settings FOR ALL
USING (
(auth.jwt() -> 'app_metadata' ->> 'role') = 'admin'
);
在服务端设置自定义声明:
// Server-side only — requires service role key
const { error } = await supabaseAdmin.auth.admin.updateUserById(userId, {
app_metadata: { role: 'admin' }
})
策略模式 — 按组织范围控制访问:
CREATE POLICY "Org members can read projects"
ON public.projects FOR SELECT
USING (
EXISTS (
SELECT 1 FROM public.members
WHERE members.organization_id = projects.organization_id
AND members.user_id = auth.uid()
)
);
关键区别 — USING 与 WITH CHECK:
USING (expr)— 筛选用户可以查看的现有行(SELECT、UPDATE、DELETE)WITH CHECK (expr)— 验证新建或修改的行数据(INSERT、UPDATE)- 对于 UPDATE,两者都需要:USING 控制哪些行可以作为操作目标,WITH CHECK 控制新值必须满足的条件
步骤 3 — 加固 API 面
**JWT 验证:**Supabase 会自动在服务端验证 JWT。RLS 策略中的 auth.uid() 函数会从已验证的 JWT 中提取已认证用户的 ID。你无需在 RLS 策略中手动验证令牌——Supabase 会处理这一过程。
**防止 SQL 注入:**Supabase JS SDK 内部使用参数化查询。切勿根据用户输入拼接原始 SQL 字符串——始终使用 SDK 查询构建器:
// SAFE: SDK parameterizes automatically
const { data } = await supabase
.from('posts')
.select('*')
.eq('author_id', userId)
.ilike('title', `%${searchTerm}%`)
// DANGEROUS: raw SQL with string interpolation
// Only use supabase.rpc() with parameterized functions, never template literals
**网络限制:**在 Dashboard > Settings > Database > Network Restrictions 中,将直接数据库连接限制为已知 IP 地址范围。这不会影响 REST API(它通过 PostgREST 连接),但可以保护直接的 Postgres 连接。
**CORS 配置:**在 Dashboard > Settings > API > CORS 中为每个项目配置允许的来源。默认允许所有来源(*)——在生产环境中应限制为你的域名。
**禁用未使用的身份验证提供方:**Dashboard > Authentication > Providers。禁用当前未使用的任何提供方(email、phone、Google、GitHub 等),以缩小攻击面。
**强制使用 SSL:**Dashboard > Settings > Database > SSL Configuration。为所有直接数据库连接强制启用 SSL。
**语句超时:**防止长时间运行的查询耗尽数据库资源:
ALTER ROLE authenticated SET statement_timeout = '10s';
ALTER ROLE anon SET statement_timeout = '5s';
撤销默认 schema 授权(仅验证):
-- Supabase handles this by default, but verify:
-- anon and authenticated roles should only access data through RLS policies
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE table_schema = 'public'
AND grantee IN ('anon', 'authenticated')
ORDER BY table_name, grantee;
输出
完成这些步骤后,你将拥有:
- 仅用于客户端代码的 anon key,以及仅限服务端使用的 service role key
- 所有 public 表均启用了 RLS,并为每种操作配置了明确的策略
- 已配置用于基于角色的访问模式的自定义 JWT 声明
- 已强化网络限制、CORS、SSL 和语句超时设置
- 已禁用未使用的 auth 提供商
安全审计清单
- 所有 public 表均已启用 RLS(
SELECT rowsecurity FROM pg_tables WHERE schemaname='public') - 每张表都为所需的每种操作至少配置了一条 RLS 策略
- service role key 未出现在任何客户端环境变量或
NEXT_PUBLIC_*环境变量中 -
.env文件已加入.gitignore - 已启用电子邮件确认(Dashboard > Authentication > Settings)
- OAuth 重定向 URL 已限制为你的域名
- 已禁用未使用的 auth 提供商
- 已启用 SSL 强制执行(Dashboard > Database > SSL)
- 数据库密码已从默认值修改
- 已为直接数据库连接配置网络限制
- 已为
authenticated和anon角色设置statement_timeout - 已为敏感用户操作启用 MFA
- 已为生产环境启用时间点恢复(PITR)
- 任何疑似泄露后都已轮换 API 密钥
错误处理
| 错误 | 原因 | 解决方案 |
|---|---|---|
42501: new row violates row-level security policy | 缺少 RLS 策略,或 WITH CHECK 条件不满足 | 为该操作添加或修复 RLS 策略;确认 auth.uid() 与行中的用户列匹配 |
查询返回空的 data,且没有错误 | RLS 的 USING 子句筛除了所有行 | 确认策略中的 auth.uid() 与已认证用户匹配;检查 JWT 声明 |
PGRST301: JWSError | JWT 令牌无效或已过期 | 重新认证用户;确认 SUPABASE_ANON_KEY 与项目匹配 |
PGRST302: anonymous access disabled | 客户端初始化时未提供 anon key | 将 anon key 传给 createClient();检查是否已设置环境变量 |
permission denied for table X | 已启用 RLS,但没有匹配的策略 | 为特定操作(SELECT/INSERT/UPDATE/DELETE)创建策略 |
Could not find the function auth.uid() | 在 PostgREST 上下文之外运行 SQL | auth.uid() 仅适用于由 PostgREST 评估的 RLS 策略;在迁移中使用显式用户 ID |
示例
新表的最简安全配置:
-- 1. Create table
CREATE TABLE public.notes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) NOT NULL DEFAULT auth.uid(),
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- 2. Enable RLS immediately
ALTER TABLE public.notes ENABLE ROW LEVEL SECURITY;
-- 3. Add policies
CREATE POLICY "Users manage own notes" ON public.notes
FOR ALL USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
客户端查询(anon key——强制执行 RLS):
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// This only returns notes belonging to the authenticated user
// because the RLS policy filters by auth.uid()
const { data: notes, error } = await supabase
.from('notes')
.select('*')
.order('created_at', { ascending: false })
资源
后续步骤
- 使用
supabase-prod-checklist应用生产环境强化配置 - 使用
supabase-auth-flows设置 auth 流程 - 使用
supabase-migrations配置数据库迁移