[控场AI]
· 12 分钟阅读· 6,407 字

Cursor 为修 Firestore 权限报错直接开放规则,隐患有多大

Cursor 为修 Firestore 权限报错直接开放规则,隐患有多大

AI 编辑器遇 Firestore 权限报错常把规则改开放,真正该修的是查询。

文章指出,Cursor、Claude Code 等 AI 编辑器在遇到 `FirebaseError: Missing or insufficient permissions` 时,倾向于直接把 `firestore.rules` 改成 `allow read, write: if true` 或仅要求登录,前者让数据对所有人开放,后者让所有注册用户可读写。真正的根源在于 Firestore 安全规则不是过滤器,查询是全有或全无——报错通常是因为查询缺少与规则匹配的 `where()` 条件。正确做法是把所有者 uid 写入每个文档,规则比较 `request.auth.uid` 与 `resource.data.ownerId`,查询补上 `where('ownerId', '==', uid)`,并在查询前等待 auth 就绪。由于 Firebase API key 本就可公开,数据安全完全依赖规则,因此可通过 emulator 的 `assertFails` 测试或 SafeWeave 等工具在数据暴露前拦住这类改动。

TL;DR

用 AI 编辑器给 Firebase 应用加功能时,遇到 FirebaseError: Missing or insufficient permissions,Cursor、Claude Code 这类工具常见的"修复"方式是把 firestore.rules 改成 allow read, write: if true; 或 if request.auth != null;。前者意味着任何人都能读写你的全部数据,后者意味着任何注册用户都能读写。真正的修复点通常在查询语句,而不是安全规则。

Firestore 权限报错

问题现场:一个文件,所有人都进来了

你让 Cursor 或 Claude Code 给 Firebase 应用加一个订单页面,页面加载时控制台报出 FirebaseError: Missing or insufficient permissions。你把错误贴回对话窗口,下一次尝试页面就能正常加载了。

Diff 只改了一个文件——firestore.rules。它现在允许所有人进入。

这不是偶发现象。AI 编辑器在处理 Supabase RLS 错误、Stripe 签名错误时也会做同样的动作:错误本身是防线在起作用,而让错误消失的最快路径就是把防线拆掉。模型盯着报错信息找"哪里写着权限",规则文件恰好就在那儿。

两种危险的"修复"写法

AI 编辑器生成的规则通常落在两个版本里,两种都能让报错消失。

第一种,完全不设防:

// firestore.rules
// CWE-862: no authorization check at all
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if true;
    }
  }
}

Firebase 官方"避免不安全规则"文档就展示了这段规则集,并附上警告"永远不要在 production 使用这套规则"。它意味着任何知道你 project ID 的人都能读取、修改或删除每个文档。

第二种,看起来负责任:

// firestore.rules
// Signed in is not the same as allowed
match /orders/{orderId} {
  allow read, write: if request.auth != null;
}

它提到了 auth,所以让人感觉像是个修复。它也确实出现在 Firebase 官方"Cloud Firestore 安全规则入门"文档里,这也是它被大量使用的原因。但官方不安全规则页面把它列在"不推荐:任何登录用户都对整个数据库有读写权限"下面。任何人都能在你的应用里注册账号,因此任何人都能读取所有订单、编辑别人的订单。

project ID 从来不是秘密。Firebase API key 文档明确说,key "可以包含在代码或已提交的配置文件中",因为数据访问由安全规则和 App Check 控制。所以在 if true 之下,完整的攻击就是一条命令:

curl "https://firestore.googleapis.com/v1/projects/YOUR_PROJECT_ID/databases/(default)/documents/orders"

不需要 key,不需要 token,返回的就是你的订单集合。

Firestore 安全规则是 Firebase 用于控制客户端对 Cloud Firestore 数据读写权限的声明式语言,文件名为 firestore.rules。规则按 match 路径逐层匹配,每个 match 块内通过 allow read, write: if <条件> 声明何时放行。条件中可用的变量包括 request.auth(当前登录用户的 uid、token 等)、resource.data(数据库中已有文档的内容)、request.resource.data(本次写入提交的内容)。规则由 Firebase 服务端强制执行,客户端代码无法绕过,因此它是保护数据的唯一防线——客户端 SDK 本身不提供权限控制。理解这套表达式语法,是判断 AI 生成规则是否安全的前提:if true 放行所有人,if request.auth != null 放行所有登录用户,只有比较 request.auth.uid 与文档中的所有者字段,才真正实现按用户隔离。

根源:错的是查询,不是规则

AI 编辑器打开规则,是因为错误信息指向规则,而真实原因通常藏在查询里。Firestore 规则不是过滤器。官方"安全查询数据"文档说得很直接:安全规则不是过滤器,查询是全有或全无。只要一个查询可能返回哪怕一条用户无权读取的文档,整个查询就会失败。

开发者写好了仅限所有者的规则,而 AI 写出这样的查询:

const snap = await getDocs(collection(db, 'orders'));

Firestore 会拒绝它。查询请求的是所有订单,而规则只允许你自己的订单。报错是正确的。但模型看到"insufficient permissions"旁边放着一个规则文件,于是把规则放宽到查询通过为止。

还有两个因素在推波助澜。创建 Firestore 数据库时,Firebase 提供测试模式,授予所有用户访问权限,所以大量公开项目和教程都是从开放规则起步的。GitHub 上关于这个错误的 issue 里,经常出现"我的规则已经设成 if true 了"作为调试步骤,让 if true 看起来像是个正常可以尝试的东西。

编辑器里没有任何东西会提示这些风险。规则文件不是 JavaScript,没有类型错误,应用也能跑。

"安全规则不是过滤器"是理解 Firestore 权限报错的关键。过滤器的语义是"查询所有数据,把无权读取的行悄悄剔除"——许多从 SQL 或 Supabase RLS 转来的开发者默认如此预期。而 Firestore 的语义是全有或全无:Firestore 会在执行查询前先判断"这个查询可能返回的文档集合,是否全部落在规则允许的范围内",只要存在任何一条用户无权读取的潜在结果,整个查询就直接被拒绝并抛出 Missing or insufficient permissions,而不是返回部分数据。这意味着规则和查询必须互相匹配:如果规则要求 ownerId == request.auth.uid,查询就必须显式带上 where('ownerId', '==', uid),让 Firestore 能在下发前确认结果集合法。缺少这个 where() 时,放宽规则并不能解决问题,只会掩盖问题。

真正的修复:规则绑定所有者,查询匹配规则

写检查所有权的规则,然后让每个查询符合它们。把所有者 uid 存到每个文档上,与 request.auth.uid 比较。

规则:

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    match /orders/{orderId} {
      allow read, delete: if request.auth != null
                          && request.auth.uid == resource.data.ownerId;
      allow create: if request.auth != null
                    && request.auth.uid == request.resource.data.ownerId;
      allow update: if request.auth != null
                    && request.auth.uid == resource.data.ownerId
                    && request.resource.data.ownerId == resource.data.ownerId;
    }

    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow update: if request.auth != null && request.auth.uid == userId
                    && request.resource.data.diff(resource.data)
                         .affectedKeys().hasOnly(['displayName', 'photoURL']);
    }
  }
}

orders 上的 update 规则还能阻止用户通过修改 ownerId 把文档转给别人。users 上的 affectedKeys().hasOnly() 检查阻止用户把 role 或 plan 之类的字段写进自己的资料。

匹配这些规则的查询:

import { collection, query, where, getDocs, addDoc } from 'firebase/firestore';

const uid = auth.currentUser.uid;

// Reads: the where() matches the rule, so Firestore allows it
const q = query(collection(db, 'orders'), where('ownerId', '==', uid));
const snap = await getDocs(q);

// Writes: always set ownerId to the signed-in user
await addDoc(collection(db, 'orders'), { ownerId: uid, item, total });

查询前要确保等 auth 就绪。在 onAuthStateChanged 触发前执行的查询没有 request.auth,即使规则正确也会报同样的错误。

用测试证明它

跑一次上面那条开放访问的 curl 命令,确认它返回权限错误。用第二个测试用户登录,确认无法按 ID 读取第一个用户的订单。Firebase Emulator Suite 配合 @firebase/rules-unit-testing 可以把这两项检查固定成带 assertFails 的测试,这样下一次 AI 对 firestore.rules 的编辑会先让构建失败,而不是让你的数据先暴露。

如果管理员需要看到所有订单,不要加一个用户可以自己写的 isAdmin 字段。用 Cloud Function 的 setCustomUserClaims 设置自定义 claim,在规则里检查 request.auth.token.admin == true。

resource 与 request.resource 的区别是写对规则时最容易混淆的一点。resource.data 表示数据库中此刻已存在的文档,在 read、update、delete 中可用;request.resource.data 表示本次请求准备写入的文档,在 create、update 中可用。据此可以区分不同操作的检验对象:create 时没有旧文档,只能检查 request.resource.data.ownerId;update 时要同时检查旧文档归属(resource.data.ownerId 等于当前用户)和新文档不会易主(request.resource.data.ownerId 仍等于旧值),从而防止把文档"过户"给别人。affectedKeys().hasOnly([...]) 则进一步限制本次更新允许触碰的字段,用来阻止用户篡改 role、plan 等敏感字段。写规则时先想清楚"这是读旧数据还是写新数据",再选择对应的变量,能避免大量逻辑漏洞。

FAQ

Q:把 Firebase API key 放在客户端代码里安全吗? A:安全。Firebase 文档说 Firebase API key 可以包含在代码里。这正是为什么安全规则必须承担起保护责任:key 标识的是你的项目,不是保护你的数据。

Q:allow read, write: if request.auth != null 安全吗? A:只有在该路径下每个登录用户都应该查看和编辑每个文档时才安全。对于用户拥有的数据,还需要求 request.auth.uid 与文档上的所有者字段匹配。

Q:收紧规则后,为什么我的查询会以"Missing or insufficient permissions"失败? A:Firestore 规则不是过滤器。添加一个匹配规则条件的 where() 子句,比如 where('ownerId', '==', uid),让查询只能返回用户有权读取的文档。

一个更前置的防线

如果希望这类问题在交付前就被拦下,可以借助 SafeWeave 这类以 MCP server 形式接入 Cursor 和 Claude Code 的工具,其 Firebase 检查会标出 if true 规则(firebase-firestore-open-rules)、仅认证写入(firebase-auth-only-write)以及开放的 Storage 和 Realtime Database 规则。App URL 扫描还能检查线上 Firestore 集合是否可被任何人读取。

即便不用工具,在 firestore.rules 里 grep 一下 if true,再加一个 emulator 测试,也能抓住本文大部分内容。关键是在数据公开之前抓住它,用什么工具反而次要。

#Cursor Firestore 权限错误#firestore.rules 安全规则#Firebase Missing or insufficient permissions#Firestore 规则不是过滤器#AI 编辑器安全风险#Firebase 数据泄露#Firestore 权限修复#Claude Code Firebase
分享:

相关推荐