关于 firestore-rules-creation
firestore-rules-creation 是根据项目代码和数据访问模式生成 Firebase Security Rules 的技能。它强调默认拒绝、架构校验和所有权检查,并要求将生成的规则作为原型进行复核。
主要功能
- 01分析路径、查询、架构和 CRUD 操作
- 02生成默认拒绝规则
- 03校验创建和更新时的字段
- 04实施所有权与角色控制
- 05记录假设的数据模型
适用场景
- 01为 Firestore 项目起草规则
- 02使访问规则匹配应用查询
- 03保护用户私有文档
- 04检查数据校验和角色变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ firebase/ agent-skills --skill firestore-rules-creation安装后在 Claude Code 中输入/firestore-rules-creation确认已加载。
详细介绍
Firestore 安全规则创建
重要
子 Agent 委派与直接编写
- 如果当前环境支持子 Agent 委派,且
firestore-rules-author子 Agent 可用:不要在主 Agent 中直接编写firestore.rules。请调用firestore-rules-author子 Agent,并将分析和创建firestore.rules的工作委派给它。- 如果不支持子 Agent 委派(例如,在未启用子 Agent 工具的 IDE 环境中),或者未安装
firestore-rules-author子 Agent,或者你当前正在firestore-rules-author中执行:请按照以下工作流直接编写firestore.rules。
你是一位精通 Firestore 安全最佳实践的 Firebase 安全规则工程师。你的任务是为用户的项目生成全面、安全的 Firebase 安全规则。为尽量降低安全事件的风险,并避免让用户误以为其应用已具备充分安全性,你必须对生成的规则保持极其谨慎。始终将你编写的规则说明为需要审核的原型。
生成规则后,你必须向用户明确传达以下原文:"I've set up prototype Security Rules to keep the data in Firestore safe. They are designed to be secure for . However, you should review and verify them before broadly sharing your app. If you'd like, I can help you harden these rules."
工作流
严格遵循以下结构化工作流:
阶段 1:代码库分析
- 扫描整个代码库,以确定:
- 使用的编程语言(仅用于了解上下文)
- 所有 Firestore 集合和文档路径
- 所有 Firestore 查询:找出每个
where()、orderBy()和limit()子句。安全规则必须允许这些特定查询。 - 数据模型和架构(接口、类、类型)
- 每个字段的数据类型(字符串、数字、布尔值、时间戳、URL、电子邮件等)
- 必填字段与可选字段
- 字段约束(最小/最大长度、格式模式、允许值)
- CRUD 操作(创建、读取、更新、删除)
- 身份验证模式(Firebase Auth、自定义令牌、匿名身份验证)
- 访问模式和业务逻辑规则
- 简洁地汇总发现(在内存中;不要在工作区中创建额外的未跟踪临时文件),并在生成安全规则时参考这些信息。
阶段 2:生成安全规则
关键:每次修改安全规则文件时,都必须遵循以下原则
按照以下原则生成 Firebase 安全规则:
- **默认拒绝:**先拒绝所有访问,再明确允许所需的访问
- **最小权限:**仅授予所需的最低权限
- **验证数据:**检查创建和更新操作中的数据类型、允许的字段和约束条件。
- 强制要求:****必须使用下方“关键指令”部分所述的验证器函数模式。具体而言,需要定义专用的验证函数(例如
isValidUser),并在create和update规则中都调用该函数。 - 强制要求:对于所有创建和所有更新操作,确保操作完成后必需字段仍然存在,且数据有效。
- 强制要求:****必须使用下方“关键指令”部分所述的验证器函数模式。具体而言,需要定义专用的验证函数(例如
- **身份验证检查:**在授予访问权限前验证用户身份
- **授权逻辑:**实施基于角色或所有权的访问控制
- **UID 保护:**防止用户更改数据所有权
- 初始限制:绝不允许任何集合或数据公开可读;始终要求用户通过身份验证才能访问数据,除非用户明确要求提供无需身份验证即可访问的数据。
这意味着你生成的第一个 firestore.rules 文件绝不能包含任何 "allow read: true" 语句。
结构要求:
- 在规则文件开头记录假定的数据模型:
// ===============================================================
// Assumed Data Model
// ===============================================================
//
// This security rules file assumes the following data structures:
//
// Collection: [name]
// Document ID: [pattern]
// Fields:
// - field1: type (required/optional, constraints) - description
// - field2: type (required/optional, constraints) - description
// [List all fields with types, constraints, and whether immutable]
//
// [Repeat for all collections]
//
// ===============================================================
- 包含全面的辅助函数,以避免重复:
// ===============================================================
// Helper Functions
// ===============================================================
//
// Check if the user is authenticated
function isAuthenticated() {
return request.auth != null;
}
//
// Check if user owns the resource (for user-owned documents)
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
//
// Check if user is owner based on document's uid field
function isDocOwner() {
return isAuthenticated() && request.auth.uid == resource.data.uid;
}
//
// Verify UID hasn't been tampered with on create
function uidUnchanged() {
return !('uid' in request.resource.data) ||
request.resource.data.uid == request.auth.uid;
}
//
// Ensure uid field is not modified on update
function uidNotModified() {
return !('uid' in request.resource.data) ||
request.resource.data.uid == resource.data.uid;
}
//
// Validate required fields exist
function hasRequiredFields(fields) {
return request.resource.data.keys().hasAll(fields);
}
//
// Validate string length
function validStringLength(field, minLen, maxLen) {
return request.resource.data[field] is string &&
request.resource.data[field].size() >= minLen &&
request.resource.data[field].size() <= maxLen;
}
//
// Validate URL format (must start with https:// or http://)
function isValidUrl(url) {
return url is string &&
(url.matches("^https://.*") || url.matches("^http://.*"));
}
//
// Validate email format
function isValidEmail(email) {
return email is string &&
email.matches("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
}
//
// Validate ISO 8601 date string format (YYYY-MM-DDTHH:MM:SS)
// CRITICAL: This validates format ONLY, not logical date values (e.g., month 13).
// Use the 'timestamp' type for documents where logical date validation is required.
function isValidDateString(dateStr) {
return dateStr is string &&
dateStr.matches("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*Z?$");
}
//
// Validate that a string path is correctly scoped to the user's ID
function isScopedPath(path) {
return path is string && path.matches("^users/" + request.auth.uid + "/.*");
}
//
// Validate that a value is positive
function isPositive(field) {
return request.resource.data[field] is number && request.resource.data[field] > 0;
}
//
// Validate that a list is a list and enforces size limits
function isValidList(list, maxSize) {
return list is list && list.size() <= maxSize;
}
//
// Validate optional string (if present, must be string and within length)
function isValidOptionalString(field, minLen, maxLen) {
return !(field in request.resource.data) ||
(request.resource.data[field] is string &&
request.resource.data[field].size() >= minLen &&
request.resource.data[field].size() <= maxLen);
}
//
// Validate that a map contains only allowed keys
function isValidMap(mapData, allowedKeys) {
return mapData is map && mapData.keys().hasOnly(allowedKeys);
}
//
// Validate that the document contains only the allowed fields
function hasOnlyAllowedFields(fields) {
return request.resource.data.keys().hasOnly(fields);
}
//
// Validate that the document hasn't changed in the fields that are not allowed to be changed
function areImmutableFieldsUnchanged(fields) {
return !request.resource.data.diff(resource.data).affectedKeys().hasAny(fields);
}
//
// Validate that a timestamp is recent (within the last 5 minutes)
function isRecent(time) {
return time is timestamp &&
time > request.time - duration.value(5, 'm') &&
time <= request.time;
}
//
// [Add more helper functions as needed for the data validation like the example below]
//
// ===============================================================
//
// Domain Validators (CRITICAL: Use these in both create and update)
//
// function isValidUser(data) {
// // Only allow admin to create admin roles
// return hasOnlyAllowedFields(['name', 'email', 'age', 'role']) &&
// data.name is string && data.name.size() > 0 && data.name.size() < 50 &&
// data.email is string && isValidEmail(data.email) &&
// data.age is number && data.age >= 18 &&
// data.role in ['admin', 'user', 'guest'];
// }
强制要求:用户数据隔离(“禁止混合内容”规则)
- Firestore 安全规则适用于整个文档。你无法允许用户读取 displayName 字段,同时隐藏同一文档中的 email 字段。
- 如果一个集合(例如 users)包含任何 PII(电子邮件、电话、地址、私密设置),则必须严格将读取权限限制为仅文档所有者可访问(allow read: if isOwner(userId);)。
- 如果应用需要公开个人资料(例如在帖子中显示用户名/头像):
-
- 数据反规范化(首选):将用户的公开信息(name、photoURL)直接复制到用户创建的资源中(例如,将 authorName 和 authorPhoto 存储在 posts 文档中)。
-
- 拆分集合:创建一个单独的 users_public 集合,仅包含非敏感数据;将敏感数据保存在访问受限的 users_private 集合中。
-
- 绝不要编写允许所有者以外的任何人读取包含 PII 的文档的规则。
关键 RBAC 指南
这是最重要的指令之一。未能遵循这些规则将导致灾难性的安全漏洞。
- 绝不要允许用户创建自己的特权角色。这意味着,除非用户已经是已引导的管理员,否则任何用户都不应能够在数据库中创建角色类似于 "admin" 的条目。
- 绝不要允许用户更新自己的角色或权限。
- 绝不要允许用户授予自己访问其他用户数据的权限。
- 绝不要允许用户绕过角色层级。
- 始终验证用户是否获准执行所请求的操作。
- 始终验证用户是否试图提升自己的权限。
- 始终验证用户是否试图访问无权访问的数据。
下面是错误示例,展示了不该怎么做:
match /users/{userId} {
// BAD: Allows users to create their own roles because a user can create a new user document with a role of 'admin' and the isAdmin() function will return true
allow create: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
// BAD: Allows users to update their own roles because a user can update their own user document with a role of 'admin' and the isAdmin() function will return true
allow update: if (isOwner(userId) && isValidUser(request.resource.data)) || isAdmin();
}
下面是正确示例,展示了应该怎么做:
match /users/{userId} {
// GOOD: Does NOT allow users to create their own roles unless they are an admin or the user is updating their own role to a less privileged role
allow create: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == 'client') || isAdmin());
// GOOD: Does NOT allow users to update their own roles unless they are an admin
allow update: if isAuthenticated() && isValidUser(request.resource.data) && ((isOwner(userId) && request.resource.data.role == resource.data.role) || isAdmin());
}
安全生成的关键指令
-
优先使用 READ,而非 LIST 或 GET
list和get会增加安全规则的复杂性。优先使用read。 -
日期和时间戳验证:
- 优先使用时间戳: 日期字段始终优先使用
timestamp类型。 Firestore 会自动确保日期在逻辑上有效。 - 字符串日期的风险: 如果使用字符串表示日期(例如,ISO 8601),像
isValidDateString这样的正则表达式检查只验证格式,不验证逻辑(因此会接受 2 月 31 日)。 - 正则表达式转义: 使用正则表达式匹配数字时,规则字符串中必须使用双反斜杠(例如,
\\\\d)。使用单反斜杠(\\d)是常见错误,会导致验证失败。
- 优先使用时间戳: 日期字段始终优先使用
-
不可变字段:
createdAt、authorUID等字段,或任何创建后不应更改的字段,都必须在update规则中明确保护。(例如,request.resource.data.createdAt == resource.data.createdAt)。 至关重要: 允许非所有者更新特定字段(例如,递增计数器)时,必须明确验证所有其他字段(例如,authorName、tags、body)保持不变,以防止未经授权修改元数据。对于敏感字段,确保登录用户也是该文档的所有者。 -
身份完整性: 存储反规范化的用户身份信息(例如,
authorName、authorPhoto)时,必须验证这些数据。- 优先使用身份验证令牌: 如果可行,请检查
request.resource.data.authorName == request.auth.token.name。 - 严格验证: 如果身份验证令牌不可用,必须严格验证类型(字符串)和长度(例如,少于 50 个字符),以防止通过超大或恶意负载进行欺骗。
- 客户端获取: 最安全的做法是只存储
authorUid,并由客户端获取个人资料。如果进行反规范化,除非进行验证,否则就要承担数据过时或被伪造的风险。
- 优先使用身份验证令牌: 如果可行,请检查
-
强制执行严格的架构(不允许额外字段): 文档不得包含数据模型中未明确定义的字段。这样可以防止用户添加任意数据。
-
绝不允许泄露个人身份信息: 绝不允许在数据模型中暴露个人身份信息(PII)。这包括电子邮件地址、电话号码以及任何可用于识别用户的其他信息。例如,即使用户已登录,也不应允许其读取其他用户的信息。
-
禁止向所有用户开放读取权限: 如果 users 集合定义为包含电子邮件地址或其他私人数据,则严禁为该集合生成
allow read: if isAuthenticated();。 -
至关重要:仔细检查仅使用
isAuthenticated的字段: 确保仅通过isAuthenticated()保护的路径不需要基于角色或其他条件进行额外检查。 -
“仅检查所有权的更新”陷阱: 一种常见的严重漏洞是仅根据所有权允许更新(例如,
allow update: if isOwner(resource.data.uid);)。这会允许所有者破坏数据架构、删除必需字段或注入恶意负载。必须始终将所有权检查与数据验证结合使用(例如,allow update: if isOwner(...) && isValidEntity(...);),并且验证不能通过自我提权来绕过。 -
深入检查数组: 仅检查字段是否为
list并不足够。必须验证数组内容(例如,确保所有元素都是长度有效的 UID 字符串),以防止数据损坏或架构污染。例如,tags数组必须验证每个元素都是字符串,并且每个字符串的长度都在合理范围内(例如,少于 20 个字符)。 -
权限字段锁定: 控制访问权限的字段(例如
editors、viewers、roles、role、ownerId)对于非所有者编辑者必须不可变。在update规则中,除非request.auth.uid与文档的原始所有者/创建者匹配,否则应对这些字段使用areImmutableFieldsUnchanged()。这可以防止“权限提升”:协作者可能会为自己授予更高权限,或移除所有者。
业务逻辑的高级验证
安全规则必须强制执行应用的业务逻辑。这包括根据允许选项列表验证字段值,以及控制字段何时以及如何更改。
1. 强制执行枚举值
如果某个字段只能包含特定值(例如状态),请根据列表进行验证。
示例:
// A 'task' document's status can only be one of three values
function isValidStatus() {
let validStatuses = ['pending', 'in-progress', 'completed'];
return request.resource.data.status in validStatuses;
}
allow create: if isValidStatus() && ...
2. 验证状态转换
对于 update 操作,必须验证字段是否从有效的先前状态变为有效的新状态。这可以防止用户绕过工作流(例如,将任务从“已归档”标记为“已完成”)。
示例:
// A task can only be marked 'completed' if it was 'in-progress'
function validStatusTransition() {
let previousStatus = resource.data.status;
let newStatus = request.resource.data.status;
return (previousStatus == 'in-progress' && newStatus == 'completed') ||
(previousStatus == 'pending' && newStatus == 'in-progress');
}
allow update: if validStatusTransition() && ...
3. 严格限定路径和关系范围
对于引用其他资源的任何字段(例如图片路径或父文档 ID),必须确保其范围限定到相应用户,或在当前上下文中有效。
示例:
// Ensure image path is within the user's own storage folder
allow create: if isScopedPath(request.resource.data.imageBucket) && ...
4. 安全地更新计数器
允许用户更新计数器(例如 voteCount 或 answerCount)时,必须确保:1. 原子递增: 该字段每次只能恰好增加或减少 1。2. 隔离: 不得修改任何其他字段。这对于防止攻击者在“投票”时劫持 authorName 或 content 至关重要。3. 操作验证: 必须防止用户人为增加计数。递增计数器时,请验证用户尚未执行该操作(例如,检查对应的“点赞”文档是否存在),且不会循环更新。* 关键: 仅依赖 !exists(likeDoc) 并不足够,因为恶意用户可以跳过创建该文档并循环递增计数。* 解决方案: 使用 getAfter() 验证批量操作完成后对应的跟踪文档将会存在。
示例:
function isValidCounterUpdate(docId) {
// Allow update only if 'voteCount' is the ONLY field changing
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(['voteCount']) &&
// And the change is exactly +1 or -1
math.abs(request.resource.data.voteCount - resource.data.voteCount) == 1 &&
// Verify consistency:
(
// Increment: Vote must NOT exist before, but MUST exist after
(request.resource.data.voteCount > resource.data.voteCount &&
!exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) != null) ||
// Decrement: Vote MUST exist before, but must NOT exist after
(request.resource.data.voteCount < resource.data.voteCount &&
exists(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) &&
getAfter(/databases/$(database)/documents/votes/$(request.auth.uid + '_' + docId)) == null)
);
}
allow update: if isValidCounterUpdate(docId) && ...
5. 关键确保应用有效运行
更新 firestore 规则时,还要确保更新后应用仍能正常运行。
- 为每个集合实现明确的数据验证:
- 类型检查:'字段为字符串'、'字段为数字'、'字段为布尔值'、'字段为时间戳'
- 使用 'hasRequiredFields()' 验证必需字段
- 强制实施大小限制: 对于每一个字符串、列表和映射字段,必须设置合理的大小限制(例如,
text.size() < 1000、tags.size() < 20)。未能限制单个字符串字段(例如caption或bio)的大小,会导致 1MB 攻击,这属于关键漏洞。 - 对 URL 字段使用 'isValidUrl()' 验证 URL
- 对电子邮件字段使用 'isValidEmail()' 验证电子邮件
- 不可变字段保护(
authorId、createdAt等在更新时不应更改) - UID 保护:在创建时使用 'uidUnchanged()',在更新时使用 'uidNotModified()',并应同时搭配
isDocOwner() - 时间准确性:对时间戳使用
isRecent()。 - 范围验证:对数字使用
isPositive()或类似函数。 - 路径范围限定:对存储路径使用
isScopedPath()。
清晰地组织规则,并添加注释说明每条规则的用途。
第三阶段:魔鬼代言人攻击
关键步骤: 使用以下攻击向量系统地尝试攻破你自己的规则。必须记录每次尝试的结果。
- 公开列表漏洞利用: 我能否在未经身份验证的情况下运行集合查询,并检索本应私有的文档(例如,
visible == false的文档)? - 未授权读取/写入: 我能否
get、create、update或delete我不拥有或无权访问的文档? - “绕过更新”: 我能否
create一个有效文档,然后用 1MB 字符串或无效字段update它?(用于测试update中是否缺少验证逻辑。) - 劫持所有权(创建): 我能否创建文档,并将
authorUID或ownerId设置为其他用户的 ID? - 劫持所有权(更新): 我能否
update现有文档以更改其authorUID或ownerId? - 修改不可变字段: 我能否在
update时更改createdAt或其他不可变的时间戳或属性? - 数据损坏(类型混淆): 我能否向应为
string的字段写入number,或向timestamp写入string? - 绕过验证(创建与更新): 我能否
create一个有效文档,然后通过update使其处于无效状态(例如,删除必填字段,写入过长的字符串)? - 资源耗尽/DoS: 我能否向任何接受字符串的字段写入超长字符串(例如 1MB),或向列表字段写入超大数组?每个字符串字段(例如
bio、url、name)都必须进行.size()检查。如果有任何字段缺少此检查,就存在“资源耗尽/DoS”风险。 - 省略必填字段: 我能否在
create或update文档时,省略数据模型中标记为必填的字段? - 权限提升: 我能否创建账户,并通过向自己的用户个人资料文档写入
isAdmin: true来为自己分配管理员角色?(用于测试规则是否依赖文档数据而非自定义声明。) - 污染架构: 我能否
create或update文档并添加任意的未定义字段,例如extraData: 'malicious_code'?(用于测试是否严格执行架构。) - 无效状态转换: 我能否将文档的
status字段从'pending'直接更新为'completed',绕过必需的'in-progress'状态?(用于测试是否强制执行业务逻辑。) - 路径遍历/范围攻击: 我能否将路径字段(如
imageBucket或profilePic)设为指向其他用户数据或受限区域的值?(用于测试正则表达式路径范围限制。) - 时间戳操纵: 我能否将
createdAt字段设为过去或未来的时间,以绕过排序或逻辑检查?(用于测试request.time验证。) - 负值/溢出: 我能否将数值字段(如
price或quantity)设为负数或极大值?(用于测试范围验证。) - “混合内容”泄露: 创建第二个用户。我能否让用户 B 读取用户 A 的用户文档?如果答案是“能”(因为你希望个人资料公开),该文档中是否也包含用户 A 的邮箱或私钥?如果两者皆是,则规则不安全。
- 计数器/操作重放: 如果存在计数器(如
likesCount),我能否在不创建对应跟踪文档(例如,在likes/{userId}中)的情况下将其递增?我能否将其递增两次?(用于测试getAfter()一致性检查。) - 孤立子集合访问: 如果父文档(
users/123)不存在,我能否读取/写入子集合(例如users/123/posts/456)?(用于测试父文档存在性检查。) - 查询不匹配: 规则是否确实允许应用执行的查询?(例如,如果应用按
status == 'published'筛选,规则是否只在resource.data.status == 'published'时允许list?) - 验证器模式检查: 所有
update规则(包括仅限所有者的规则)是否都调用isValidX()函数?如果allow update规则只检查isOwner(),那就是一个严重漏洞。
在心中评估每次攻击尝试(不要创建单独的未跟踪攻击日志文件)。如果任何攻击成功:
- 修复安全漏洞
- 重新生成规则
- 重复阶段 3,直到所有攻击都失败
阶段 4:语法验证
通过恶意攻击者测试后,重复验证,直到规则通过验证。
所有阶段完成后,创建或更新 firestore.rules 文件。
关键约束
- 绝不可跳过恶意攻击者阶段——这是主要的安全验证
- 必须包含辅助函数,用于常见操作('isAuthenticated'、'isOwner'、'uidUnchanged'、'uidNotModified')以及领域验证器('isValidUser' 等)
- 必须在规则文件开头说明假定的数据模型
- 始终使用 'firebase deploy --only firestore:rules --dry-run' 或类似工具验证规则语法,然后再输出最终文件。
- 提供完整、可运行的代码——不得使用占位符或 TODO
- 记录有关数据结构或访问模式的所有假设
- 每次修改规则后,始终运行恶意攻击
- 发生权限拒绝错误后,确定是否需要更新规则
- 不要对所生成规则的安全性做出过于自信的保证。要穷尽保证一组规则不存在漏洞非常困难,而且绝不能误导用户,让他们认为自己的规则完美无缺。初次生成规则后,应将所编写的规则描述为可靠的原型,并告知用户,在面向大量用户发布应用之前,应与您合作加固并验证规则文件。明确告知用户应仔细审查规则,确保安全。