Firebase Cloud Function触发器安全校验如何避免无限循环
问题根因
你当前的逻辑出现无限循环的核心原因是:Firestore 触发器无法自动区分「终端用户从客户端发起的操作」和「云函数自身执行的补偿操作」。
完整循环链路如下:
- 用户创建非法文档 → 触发
onCreate→ 校验失败执行删除 - 云函数删除文档 → 触发
onDelete→ 判定删除非法执行恢复写入 - 云函数恢复文档 → 再次触发
onCreate→ 回到第一步无限循环
可落地解决方案
以下方案按改造成本从低到高排序,可根据你的安全等级要求选择:
方案1:可信操作识别(改造成本最低,适配现有逻辑)
利用Firestore触发器的上下文特征,直接跳过云函数自身发起的可信操作,从根源切断循环:
- 客户端通过用户登录态发起的操作,触发器的
context.auth字段会携带操作用户的UID信息 - 云函数通过Admin SDK发起的服务端操作,触发器的
context.auth字段为null(无终端用户上下文,属于可信操作)
修正后的代码如下:
export const createEventTrigger = functions.firestore.document("events/{id}").onCreate(async (snapshot, context) => { // 服务端可信操作直接跳过校验,不执行补偿逻辑 if (!context.auth) return; const event = snapshot.data() as Event; const isValid = await isEventDocumentCreateValid(context, event); if (!isValid) { console.log(`Deleting invalid document for user ${context.auth.uid}`); // 该删除为服务端操作,触发onDelete时会被直接跳过,不会触发恢复 await snapshot.ref.delete(); } }); export const deleteEventTrigger = functions.firestore.document("events/{id}").onDelete(async (snapshot, context) => { // 服务端可信操作直接跳过校验,不执行补偿逻辑 if (!context.auth) return; const event = snapshot.data() as Event; const isValid = await isEventDocumentDeleteValid(context, event); if (!isValid) { console.log(`Restoring invalidly deleted document for user ${context.auth.uid}`); // 该写入为服务端操作,触发onCreate时会被直接跳过,不会触发删除 await snapshot.ref.set(event); } });
注意:该方案的前提是你必须妥善保管Admin SDK服务账号凭证,绝对不能泄露到客户端,否则攻击者可直接伪造服务端操作绕过校验。
方案2:显式系统标记(适配无法依赖context.auth的场景)
如果你的场景里存在其他可信服务操作数据库、无法通过context.auth区分,可以给所有云函数发起的操作加专属不可伪造的标记,触发器识别到标记直接跳过:
- 云函数删除非法文档前,先给文档更新
_sysMarkForDelete: true标记,再执行删除 onDelete触发时先检查快照中是否存在_sysMarkForDelete标记,存在则直接返回,不执行恢复- 云函数恢复非法删除的文档时,写入时带上
_sysRestored: true标记 onCreate触发时先检查快照中是否存在_sysRestored标记,存在则直接返回,不执行删除
注意:必须通过IAM权限限制客户端无法写入这两个系统标记字段,否则攻击者可自行添加标记绕过校验。
方案3:前置操作队列(安全性最高,无脏数据窗口)
上述两种方案都属于「事后补偿」逻辑,天然存在短时间的脏数据窗口(非法文档从写入到被删除/恢复的间隔内,可被其他用户读取到)。如果对数据一致性、安全性要求高,建议直接重构逻辑,彻底隔离用户操作和正式数据:
- 收回客户端对
events正式集合的所有写权限,仅允许云函数使用的服务账号写入正式集合 - 所有用户的创建、删除请求全部写入临时队列集合(比如
event_pending_ops),每条请求携带操作类型、操作内容、请求用户ID - 云函数仅监听队列集合的写入事件,拿到请求后做合法性校验:
- 校验通过:执行对应正式集合的写入/删除操作,将队列请求标记为成功
- 校验失败:不操作正式集合,直接将队列请求标记为失败,附上失败原因
- 客户端可监听自己提交的队列文档状态,给用户展示操作结果
该方案完全不存在触发器循环问题,也不会出现脏数据泄露的时间窗口,是生产环境的推荐方案。
注意事项
- 不管使用哪种方案,上线前必须在测试环境模拟非法创建、非法删除操作,确认不会出现循环触发,避免短时间内大量消耗云函数、Firestore读写配额产生意外账单。
- 如果你的业务允许,优先使用Firestore安全规则做前置校验,性能比云函数触发器高、成本更低,也不会存在异步补偿的时间窗口问题。
内容的提问来源于stack exchange,提问作者Jack Maring
相关产品推荐
相关产品推荐

