一位安全研究员花了 1500 美元让 14 个大模型去攻击一个故意留有漏洞的 App,结果发现 GPT-5.5 的破解率高达 70%,而绝大多数模型找到的漏洞都指向同一个地方——Firebase 的权限配置。这不是个例,根据 2026 年 OWASP 移动安全报告,超过 60% 使用 BaaS(Backend as a Service)的应用存在不同程度的权限配置错误。如果你的项目正在使用 Firebase 或 Supabase,这篇文章可能会帮你避免一次严重的数据泄露。
🔐 一、BaaS 安全漏洞全景:攻击者眼中的 Firebase
1.1 为什么 BaaS 平台成为安全重灾区?
BaaS 的核心卖点是「零后端代码」——前端直接与数据库通信,省去了传统 API 层。但这也意味着安全边界从服务端转移到了配置层。一个错误的 Firestore Security Rule、一个过于宽松的 Supabase RLS 策略,就等于把数据库大门直接敞开。
最常见的攻击模式有三种:
- ✅ 直接读取 Firestore:通过
google-services.json中的配置信息直接连接 Firebase,绕过你的 API - ✅ 匿名认证绕过:Firebase 默认允许匿名登录,攻击者可以用匿名身份访问「已认证用户」才能看的数据
- ✅ Storage 公开访问:Firebase Storage 规则配置不当,导致用户私有文件可被任何人下载
⚠️ **警告:**Firebase 的
google-services.json/GoogleService-Info.plist文件会被打包到 App 中,任何人都可以通过反编译获取你的 Firebase 项目 ID、API Key 和数据库地址。API Key 不是秘密,安全完全依赖 Security Rules。
1.2 一个真实的攻击流程
以那个 1500 美元实验中的漏洞 App 为例,攻击者只需要 4 步:
- 反编译 APK,提取
google-services.json - 获取 Firebase 项目 ID 和 API Key
- 使用 Firebase SDK 直接连接 Firestore
- 查询所有用户的私有数据
// 攻击者只需要这几行代码就能读取你的数据库
// firebase-exploit.js — 通过反编译获取的配置直接连接 Firebase
import { initializeApp } from 'firebase/app'
import { getFirestore, collection, getDocs } from 'firebase/firestore'
import { getAuth, signInAnonymously } from 'firebase/auth'
// 这些信息来自 google-services.json,不是秘密
const firebaseConfig = {
apiKey: 'AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
authDomain: 'your-project.firebaseapp.com',
projectId: 'your-project-id',
storageBucket: 'your-project.appspot.com',
messagingSenderId: '123456789',
appId: '1:123456789:android:abcdef'
}
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
const auth = getAuth(app)
// 第一步:匿名登录(如果规则允许)
await signInAnonymously(auth)
// 第二步:直接查询所有用户数据
const querySnapshot = await getDocs(collection(db, 'users'))
querySnapshot.forEach(doc => {
console.log(`用户 ${doc.id}:`, doc.data())
})
📌 **记住:**这个实验中,70% 的成功攻击都是因为 Firestore Security Rules 没有正确配置。API 本身是安全的,但数据库的大门是敞开的。
🛡️ 二、Firebase Security Rules 深度审计
2.1 最危险的规则配置 Top 5
以下是从真实项目中收集的最常见危险配置。如果你的规则命中了任何一条,请立即修复:
// firestore.rules — ❌ 危险配置 Top 5
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ❌ 危险 #1:允许所有人读写所有数据
match /{document=**} {
allow read, write: if true;
}
// ❌ 危险 #2:只检查是否登录,不限制数据归属
match /users/{userId} {
allow read, write: if request.auth != null;
}
// ❌ 危险 #3:允许用户读取其他用户数据
match /users/{userId}/reviews/{reviewId} {
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
}
// ❌ 危险 #4:Storage 规则允许所有已认证用户访问
match /b/{bucket}/o/{allPaths=**} {
allow read, write: if request.auth != null;
}
// ❌ 危险 #5:没有规则(默认拒绝,但很多人不知道需要显式配置)
// 如果你完全没有写规则,Firebase 默认是拒绝的
// 但很多教程会告诉你加上 allow read, write: if true 来「调试」
}
}
2.2 安全的 Firestore Security Rules 模板
正确的规则应该遵循最小权限原则:每个集合、每条文档都有明确的访问控制。
// firestore.rules — ✅ 生产级安全配置
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ✅ 辅助函数:检查是否为文档所有者
function isOwner(userId) {
return request.auth != null && request.auth.uid == userId;
}
// ✅ 辅助函数:验证数据格式
function isValidReview(data) {
return data.title is string
&& data.content is string
&& data.rating is number
&& data.rating >= 1 && data.rating <= 5;
}
// 用户公开资料:所有人可读,仅自己可写
match /users/{userId} {
allow read: if true;
allow create: if isOwner(userId);
allow update: if isOwner(userId)
&& request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['displayName', 'avatar', 'bio']);
allow delete: if false; // 禁止删除,用软删除
}
// 用户私有数据:仅所有者可读写
match /users/{userId}/private/{docId} {
allow read, write: if isOwner(userId);
}
// 书评:所有人可读,仅作者可写
match /reviews/{reviewId} {
allow read: if true;
allow create: if isOwner(request.resource.data.authorId)
&& isValidReview(request.resource.data);
allow update: if isOwner(resource.data.authorId)
&& request.resource.data.authorId == resource.data.authorId;
allow delete: if isOwner(resource.data.authorId);
}
// 管理员集合:仅自定义 Token 中的 admin claim
match /admin/{docId} {
allow read, write: if request.auth != null
&& request.auth.token.admin == true;
}
// 默认拒绝所有其他路径
match /{document=**} {
allow read, write: if false;
}
}
}
2.3 Firebase Storage 安全规则
Storage 的规则同样容易出错。以下是常见的安全配置:
// storage.rules — ✅ 按用户隔离的存储规则
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// 用户头像:所有人可读,仅本人可写且限制文件大小
match /avatars/{userId}/{allPaths=**} {
allow read: if true;
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 最大 5MB
&& request.resource.contentType.matches('image/.*');
}
// 用户私有文件:仅本人可读写
match /private/{userId}/{allPaths=**} {
allow read, write: if request.auth != null
&& request.auth.uid == userId;
}
// 默认拒绝
match /{allPaths=**} {
allow read, write: if false;
}
}
}
💡 **提示:**使用
firebase deploy --only firestore:rules单独部署规则,不需要重新部署整个项目。每次修改规则后,用 Firebase Emulator 测试确认行为正确。
🔍 三、Supabase RLS 安全配置与审计
Supabase 基于 PostgreSQL 的 Row Level Security(RLS)提供了比 Firebase 更细粒度的权限控制,但如果忘记启用 RLS,效果和 Firebase 的 allow all 一样糟糕。
3.1 最常见的 RLS 配置错误
-- ❌ 错误 #1:创建表后忘记启用 RLS
-- Supabase 默认新表不启用 RLS,这意味着所有已认证用户可以访问所有行
CREATE TABLE profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
email TEXT,
is_admin BOOLEAN DEFAULT false
);
-- ❌ 错误 #2:创建了 RLS 策略但忘记启用 RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- ❌ 错误 #3:策略条件过于宽松
CREATE POLICY "allow_authenticated" ON profiles
FOR ALL
USING (auth.role() = 'authenticated'); -- 所有登录用户都能看所有数据
-- ❌ 错误 #4:在 Storage 中使用过于宽松的策略
INSERT INTO storage.buckets (id, name, public)
VALUES ('avatars', 'avatars', true); -- true = 公开访问
3.2 正确的 Supabase RLS 配置
-- ✅ 正确的 RLS 配置模板
-- Step 1: 创建表并立即启用 RLS
CREATE TABLE profiles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Step 2: 所有人可读公开资料
CREATE POLICY "profiles_select_public" ON profiles
FOR SELECT USING (true);
-- Step 3: 仅本人可创建自己的资料
CREATE POLICY "profiles_insert_own" ON profiles
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Step 4: 仅本人可更新,且不能修改 user_id
CREATE POLICY "profiles_update_own" ON profiles
FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Step 5: 禁止删除(使用软删除模式)
CREATE POLICY "profiles_delete_none" ON profiles
FOR DELETE USING (false);
-- ✅ Storage Bucket 配置:私有 bucket + 按用户隔离
INSERT INTO storage.buckets (id, name, public)
VALUES ('user-files', 'user-files', false);
-- Storage RLS:用户只能访问自己文件夹下的文件
CREATE POLICY "storage_user_isolation" ON storage.objects
FOR ALL
USING (
bucket_id = 'user-files'
AND auth.uid()::text = (storage.foldername(name))[1]
);
⚡ **关键结论:**Supabase 的 RLS 比 Firebase Security Rules 更强大(支持完整的 SQL 表达式),但也更容易遗漏。建议在 CI/CD 中加入 RLS 检查脚本,确保每张表都启用了 RLS。
3.3 自动化审计脚本
以下脚本可以检查你的 Supabase 项目中哪些表没有启用 RLS:
-- audit-rls.sql — 检查所有用户表是否启用了 RLS
-- 在 Supabase SQL Editor 中运行
SELECT
schemaname,
tablename,
rowsecurity AS rls_enabled,
CASE
WHEN rowsecurity THEN '✅ RLS 已启用'
ELSE '❌ RLS 未启用 — 危险!'
END AS status
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
-- 检查每张表的 RLS 策略数量
SELECT
schemaname,
tablename,
COUNT(pol.policyname) AS policy_count,
CASE
WHEN COUNT(pol.policyname) = 0 THEN '❌ 无策略 — 需要添加'
WHEN COUNT(pol.policyname) < 2 THEN '⚠️ 策略不完整 — 至少需要 SELECT 和 INSERT'
ELSE '✅ 有 ' || COUNT(pol.policyname) || ' 条策略'
END AS status
FROM pg_tables t
LEFT JOIN pg_policies pol ON pol.tablename = t.tablename
AND pol.schemaname = t.schemaname
WHERE t.schemaname = 'public'
GROUP BY t.schemaname, t.tablename
ORDER BY t.tablename;
📊 四、BaaS 安全检查清单与对比
下表总结了 Firebase 和 Supabase 的安全机制对比,帮助你快速判断哪个平台的安全模型更适合你的项目:
| 安全维度 | Firebase | Supabase | 推荐 |
|---|---|---|---|
| 默认安全策略 | ❌ 默认允许所有访问 | ⚠️ 默认关闭 RLS | 都需要手动配置 |
| 规则语言 | 自定义 DSL(有限) | 完整 SQL(强大) | ✅ Supabase |
| 行级安全 | ✅ Security Rules | ✅ RLS(PostgreSQL 原生) | ✅ Supabase |
| 列级安全 | ❌ 不支持 | ✅ 通过 View 实现 | ✅ Supabase |
| 服务端验证 | ✅ Admin SDK 绕过规则 | ✅ Service Role Key | 平手 |
| 审计日志 | ⚠️ 需要额外配置 | ✅ PostgreSQL 原生日志 | ✅ Supabase |
| 本地安全测试 | ✅ Emulator Suite | ✅ Docker 本地环境 | 平手 |
| 规则测试框架 | ⠷ Rules Unit Testing | ⚠️ 需要自行编写 | ✅ Firebase |
💡 提示:如果你的团队有 SQL 经验,Supabase 的 RLS 会让你的审计和维护成本大幅降低。如果你是全 JavaScript 团队且项目简单,Firebase 的 Security Rules 也足够用,但一定要写单元测试。
⚠️ 五、实战避坑指南
以下是从真实项目中总结的安全教训,每一条都对应过真实的数据泄露事件:
- ✅ 永远不要在客户端代码中信任用户输入 — 即使有 Security Rules,也要在 Cloud Functions / Edge Functions 中做二次验证
- ❌ 不要在 Firestore Rules 中使用
request.auth != null作为唯一条件 — 这只检查是否登录,不检查是否有权限 - ⚠️ 注意 Firebase 的
list操作 —get检查单条文档权限,list检查集合权限,两者规则不同 - ✅ 定期运行安全审计 — 在 CI/CD 中集成 Firebase Security Rules 模拟器测试
- ❌ 不要在
.env或配置文件中硬编码 Service Account Key — 使用环境变量或 Secret Manager - ✅ 启用 Firebase App Check — 防止来自非你 App 的请求(爬虫、脚本攻击)
- ⚠️ Supabase 的
anonkey 不是秘密 — 它和 Firebase API Key 一样会暴露在客户端,安全依赖 RLS
// ✅ 正确做法:在 Cloud Functions 中做二次验证
// functions/validateAccess.js
const { onCall, HttpsError } = require('firebase-functions/v2/with')
const { getFirestore } = require('firebase-admin/firestore')
exports.getUserReview = onCall(async (request) => {
const { auth, data } = request
// 验证 1:必须已认证
if (!auth) {
throw new HttpsError('unauthenticated', '请先登录')
}
// 验证 2:只能访问自己的数据
if (data.userId !== auth.uid) {
throw new HttpsError('permission-denied', '无权访问他人数据')
}
// 验证 3:查询参数验证
if (!data.reviewId || typeof data.reviewId !== 'string') {
throw new HttpsError('invalid-argument', '参数无效')
}
const db = getFirestore()
const doc = await db
.collection('users')
.doc(data.userId)
.collection('reviews')
.doc(data.reviewId)
.get()
if (!doc.exists) {
throw new HttpsError('not-found', '评论不存在')
}
return doc.data()
})
🎯 总结
BaaS 安全的核心原则只有一条:假设客户端代码完全公开。你的 google-services.json、Supabase anon key、所有前端代码都会被攻击者看到。安全不能依赖「隐藏」,而必须依赖「验证」。
三条行动建议:
- 今天就审计 — 运行本文提供的 SQL 脚本检查你的 Supabase 项目,或用 Firebase Emulator 测试你的 Security Rules
- 建立安全 CI/CD — 每次部署前自动运行规则测试,防止规则被意外修改
- 关注 OWASP — 每年更新的 OWASP Top 10 会告诉你最新的安全威胁
🔧 相关工具推荐:
- Firebase Security Rules Unit Testing — 官方规则测试框架
- Supabase RLS Advisor — RLS 配置检查工具
- Firebase Emulator Suite — 本地安全测试环境
- OWASP Mobile Security Testing Guide — 移动安全测试权威指南