0xmrma/CVE-2026-46552
GitHub: 0xmrma/CVE-2026-46552
该项目详细记录并复现了 NocoDB 中一个授权边界漏洞(CVE-2026-46552),证明了公共共享链接可被利用来邀请真实成员并在撤销共享后保留持久访问权限。
Stars: 0 | Forks: 0
# CVE-2026-46552
NocoDB 共享 Base 链接可邀请真实 Base 成员并在撤销共享后继续保留权限
## 简介
我在审查 **NocoDB** 时发现了这个问题,当时脑海中有一个简单的安全问题:
**公共的共享 Base 链接能否跨越边界,从临时的共享访问权限转变为真实的已认证 Base 成员身份?**
在这个案例中,答案是肯定的。
一个仅通过 `xc-shared-base-id` 认证的共享 Base 会话,在 ACL(访问控制列表)层面被当作普通的 Base 查看者(viewer)对待。由于查看者权限依然能够触达成员管理接口,仅拥有共享 Base UUID 的用户就可以枚举现有的 Base 成员,并将任意电子邮件地址作为真实成员邀请进入该 Base。
随后,被邀请的用户可以通过正常的注册流程兑换邀请,获取一个标准的认证账户,并且在所有者禁用共享 Base 链接后依然保留对该 Base 的访问权限。
该问题最终成为了 **CVE-2026-46552**。
**项目:** [NocoDB](https://github.com/nocodb/nocodb)
**验证受影响的版本:** `0.301.3`
这影响了 **NocoDB**。在其官方网站上,NocoDB 宣称受到 **35,000+** 家组织的信任,下载量超过 **20+ million** 次。
网站还列出了诸如 **Accenture**、**Western Digital**、**Hyundai**、**Walmart**、**PwC**、**Bosch** 和 **American Express** 等公司。
## 攻击链
`公共共享 Base 链接 -> xc-shared-base-id 被视为普通的 Base 查看者 -> 查看者 ACL 触达成员管理接口 -> 攻击者列出 Base 用户并邀请任意电子邮件 -> 被邀请用户兑换正常的注册 token -> 持久的已认证 Base 访问权限在共享链接被撤销后依然存活`
## NocoDB 的功能
**NocoDB** 是一个面向数据库的协作平台,提供基于浏览器的 Base 访问、共享、元数据管理和用户成员管理工作流。
这意味着它的共享模型是一个真正的安全边界。
这里的重要问题不在于共享 Base 链接能否读取共享内容。
真正的问题是:
**公共共享主体(principal)能否执行本应只属于已认证 Base 成员的操作?**
在这个案例中,它可以。
## 为什么这个攻击面值得研究
公共共享功能很容易被低估。
这是一个错误。
一旦应用程序支持:
- 匿名或基于链接的访问
- 角色映射
- 以及位于同一 ACL 系统背后的普通已认证管理 API
主要风险就不仅仅是数据暴露。
更强的风险是**边界坍塌(boundary collapse)**:
- 低信任主体继承了高信任能力
- 管理操作可以从公共共享上下文中触达
- 临时访问可以转换为持久访问
这才是这里真正的问题。
这不是登录验证中的 bug。
这不是 token 伪造问题。
这不是密码重置缺陷。
这是一个经典的**授权边界失效**:
- 共享链接主体被映射到了普通的 Base 角色
- 这些角色依然包含成员管理能力
- 真正持久的访问控制状态随后可以从公共共享上下文中被修改
## 我关注的边界
我没有通过随机探测接口并期待返回有趣的内容来研究这个问题。
更强的路径是首先识别出最高价值的信任边界。
对于 NocoDB 来说,那就是以下两者之间的边界:
- **共享 Base 访问**
- 和**已认证的 Base 成员身份**
这两种状态不应该是可互换的。
共享 Base 链接本应代表具有范围限制、可撤销的、基于链接的访问。
它不应该能够在 Base 内部铸造新的长期存活的主体。
这正是此处失效的具体边界。
## 根本原因
该漏洞源于共享 Base 访问被集成到正常 ACL 路径的方式。
在共享 Base 的前端流程中,注入了 `xc-shared-base-id` 的同时移除了普通的 auth 标头。
然后,在后端,`BaseViewStrategy` 接受 `xc-shared-base-id` 并将共享链接直接转换为从共享 Base 配置派生出的普通 `roles` / `base_roles`。
这是第一个问题。
第二个问题是,**viewer 级别的权限依然包含成员管理操作**。
在 ACL 层中,`ProjectRoles.VIEWER` 可以触达:
- `baseUserList`
- `userInvite`
这些权限保护着正常的 meta 路由:
- `GET /api/v2/meta/bases/:baseId/users`
- `POST /api/v2/meta/bases/:baseId/users`
因此,公共共享会话实际上被允许访问旨在供真实 Base 参与者使用的成员身份接口。
最后一步在于邀请流程本身。
`BaseUsersService.userInvite()` 检查了角色权限,然后创建了:
- 一个带有 `invite_token` 的真实用户行
- 一个针对目标 Base 的真实 Base 成员行
而对于共享 Base 会话:
- `invited_by` 变成了 `null`
因为请求背后没有真实的已认证邀请者身份。
这就是整个 bug 链。
### 为什么这是可利用的
因为仅仅拥有共享 Base 链接就足够了。
攻击者不需要:
- `xc-auth`
- 预先存在的账户
- 被盗的凭据
- 或 Base 中先前的成员身份
利用链非常直接:
- 攻击者获取共享 Base UUID
- 该 UUID 被接受为 Base 查看者主体
- 查看者 ACL 触达成员管理接口
- 攻击者列出当前的 Base 成员
- 攻击者邀请任意电子邮件地址
- 被邀请的用户通过正常的注册流程兑换 token
- 新账户成为真实的已认证 Base 成员
- 所有者随后禁用共享链接
- 被邀请的账户依然保留正常的已认证访问权限
这将可撤销的链接共享转变成了持久的成员身份。
## 为什么这是一个安全问题,而不仅仅是奇怪的共享行为
重要的区别在于撤销后的持久性。
这不仅仅是:
漏洞主体不是普通的已认证查看者。
它是一个**公共共享会话**。
这很重要,因为应用程序将一个临时的、限定范围的链接主体视为足够受信任,以至于可以:
- 枚举真实成员
- 更改访问控制状态
- 并在 Base 内部创建新的持久主体
真正的问题不在于:
真正的问题在于:
答案是肯定的。
这就是为什么这是一个真正的授权漏洞,而不仅仅是一个令人惊讶的应用程序行为。
## PoC
我在本地针对以下环境进行了验证:
- 产品版本:`0.301.3`
- commit:`dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad`
- Base URL:`http://127.0.0.1:8080`
重现过程非常直接。
首先,我作为一个普通的所有者账户登录,创建了一个全新的 Base,创建了一个表格,并以 `viewer` 的身份启用了共享 Base 访问:
```
PATCH /api/v2/meta/bases//shared
Content-Type: application/json
{
"roles": "viewer"
}
```
这返回了共享 Base UUID。
然后,在不发送任何 `xc-auth` 的情况下,我仅使用了:
```
xc-shared-base-id:
```
仅使用该标头,我调用了:
```
GET /api/v2/meta/bases//users
```
这返回了 `200 OK` 并暴露了真实的 Base 成员,包括电子邮件地址。
依然仅使用 `xc-shared-base-id`,我调用了:
```
POST /api/v2/meta/bases//users
Content-Type: application/json
{
"email": "attacker@example.com",
"roles": "viewer"
}
```
这也返回了 `200 OK`。
对于没有电子邮件传递的本地实验室验证,我直接在 SQLite 元数据库中确认了:
- `nc_users_v2` 包含了带有非空 `invite_token` 的受邀用户
- `nc_base_users_v2` 包含了目标 Base 的真实成员行
- `invited_by` 为 `NULL`
然后我通过正常的注册流程兑换了邀请:
```
POST /api/v2/auth/user/signup
Content-Type: application/json
{
"email": "attacker@example.com",
"password": "Password123.",
"token": ""
}
```
使用返回的 `xc-auth`,我调用了:
```
GET /api/v2/meta/bases//tables
```
这返回了 `200 OK`。
最后,作为所有者,我禁用了共享 Base 链接:
```
DELETE /api/v2/meta/bases//shared
```
在这之后:
- 使用 `xc-shared-base-id` 的共享链接访问失败并返回 `401`
- 使用正常 `xc-auth` 的受邀账户依然以 `200` 成功访问
### 观察到的结果
- 共享用户列表:`200`
- 共享邀请:`200`
- 注册:`200`
- 禁用共享前受邀者的已认证表访问:`200`
- 禁用共享:`200`
- 禁用后共享链接的表访问:`401`
- 禁用后受邀者的已认证表访问:`200`
这确立了核心的安全主张:
- 公共共享访问可以触达成员身份接口
- 成员身份更改创建了真正持久的已认证访问
- 撤销原始共享并没有移除该访问权限
## 为什么复现过程很重要
一次成功的共享 Base 邀请就已经足以证明授权失败。
但完整的验证链之所以重要,有两个原因。
### 第一
它表明这不仅仅是接口暴露。
公共共享会话不仅仅是触及了一个受限的 API。
它完成了完整的权限转换链:
- 枚举成员
- 邀请新主体
- 兑换邀请
- 获取正常的已认证访问
### 第二
它证明了这不是自我撤销的。
更严重的影响发生在共享链接被禁用之后:
- 原始链接失效了
- 攻击者创建的账户却没有
这就是将临时链接访问转变为持久访问的全部原因。
## 影响
该漏洞允许任何拥有共享 Base 链接的人:
- 枚举真实的 Base 成员及其电子邮件地址
- 将任意电子邮件地址作为真实成员邀请进入 Base
- 将临时的基于链接的访问转换为持久的已认证成员身份
- 即使在所有者撤销共享链接后也能保留该访问权限
主要影响在于**机密性**,因为攻击者可以通过普通的已认证账户维持对共享 Base 数据的持久读取访问。
此外还存在**完整性影响**,因为公共共享主体可以通过向 Base 添加新成员来修改访问控制状态。
这是一个比普通数据泄露更严重的结果。
这是匿名共享与已认证成员身份之间的权限边界破坏。
## 严重性和分类
此问题被合理地归类为具有机密性影响的跨范围授权缺陷。
**CVSS:**
```
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:N/A:N
```
该向量符合此处的核心行为:
- 网络可达的行为
- 不需要预先存在的已认证账户
- 利用过程中不需要受害者的用户交互
- 范围发生改变,因为公共共享主体跨越到了正常的成员管理能力
- 通过持久的未授权 Base 访问造成机密性影响
## 建议的缓解措施
修复方向很直接。
共享 Base 会话不应继承成员管理能力。
至少应做到:
- 从任何可通过 `xc-shared-base-id` 触达的权限中移除 `baseUserList` 和 `userInvite`
- 实施显式拦截,使得共享/公共主体无法调用 Base 成员身份接口,例如 `GET` 和 `POST /api/v2/meta/bases/:baseId/users`
- 将共享 Base 访问视为一种独特的主体类型,而不是将其直接映射到普通的 Base 查看者权限
- 添加回归测试,验证共享 Base 请求无法枚举成员、无法邀请用户,也无法创建能在共享被撤销后继续存活的持久访问
## 披露
此问题已在本地针对 commit 为 `dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad` 的 NocoDB `0.301.3` 版本进行了验证。
该报告演示了:
- 从 `xc-shared-base-id` 到普通 Base 角色的 ACL 映射
- 查看者权限触达成员管理接口的路径
- 创建真实的受邀用户和 Base 成员行
- 通过正常注册流程兑换邀请的能力
- 在共享链接撤销后已认证访问的持久性
该问题被分配了:
**CVE-2026-46552**
## 这个 Bug 真正教会了我们什么
这里的关键教训很简单:
许多系统在将这两种概念合并到同一个角色模型中时,都会陷入麻烦。
共享链接在操作上可能看起来类似于查看者账户,但信任假设是不同的:
- 共享链接很容易被重新分发
- 共享链接旨在可被撤销
- 共享链接通常是较低保证级别的主体
如果这种较低保证级别的主体能够执行管理操作或铸造新的持久身份,那么共享边界就已经被破坏了。
这才是真正的核心要点。
## 关键点
- 公共共享功能是安全边界
- 共享链接主体不应继承普通的成员管理能力
- 从公共共享上下文枚举成员这本身就已经很敏感了
- 未经授权的邀请更糟糕,因为它会创建真实的持久主体
- 如果攻击者创建的账户存活下来,仅仅撤销原始共享是不够的
- 将公共共享访问视为一种单独的主体类型是更安全的设计
## 结语
这个漏洞并不是关于完全绕过身份验证。
它是关于将两个本应保持独立的信任级别坍塌在一起。
在 NocoDB 中,共享 Base 链接本应提供对共享内容的临时、可撤销的访问。
相反,它可被用于枚举成员、邀请真实用户进入 Base,并将公共共享访问转变为在共享撤销后依然存活的、持久的已认证成员身份。
这就是它成为 **CVE-2026-46552** 的原因。
## 攻击链
`公共共享 Base 链接 -> xc-shared-base-id 被视为普通的 Base 查看者 -> 查看者 ACL 触达成员管理接口 -> 攻击者列出 Base 用户并邀请任意电子邮件 -> 被邀请用户兑换正常的注册 token -> 持久的已认证 Base 访问权限在共享链接被撤销后依然存活`
## NocoDB 的功能
**NocoDB** 是一个面向数据库的协作平台,提供基于浏览器的 Base 访问、共享、元数据管理和用户成员管理工作流。
这意味着它的共享模型是一个真正的安全边界。
这里的重要问题不在于共享 Base 链接能否读取共享内容。
真正的问题是:
**公共共享主体(principal)能否执行本应只属于已认证 Base 成员的操作?**
在这个案例中,它可以。
## 为什么这个攻击面值得研究
公共共享功能很容易被低估。
这是一个错误。
一旦应用程序支持:
- 匿名或基于链接的访问
- 角色映射
- 以及位于同一 ACL 系统背后的普通已认证管理 API
主要风险就不仅仅是数据暴露。
更强的风险是**边界坍塌(boundary collapse)**:
- 低信任主体继承了高信任能力
- 管理操作可以从公共共享上下文中触达
- 临时访问可以转换为持久访问
这才是这里真正的问题。
这不是登录验证中的 bug。
这不是 token 伪造问题。
这不是密码重置缺陷。
这是一个经典的**授权边界失效**:
- 共享链接主体被映射到了普通的 Base 角色
- 这些角色依然包含成员管理能力
- 真正持久的访问控制状态随后可以从公共共享上下文中被修改
## 我关注的边界
我没有通过随机探测接口并期待返回有趣的内容来研究这个问题。
更强的路径是首先识别出最高价值的信任边界。
对于 NocoDB 来说,那就是以下两者之间的边界:
- **共享 Base 访问**
- 和**已认证的 Base 成员身份**
这两种状态不应该是可互换的。
共享 Base 链接本应代表具有范围限制、可撤销的、基于链接的访问。
它不应该能够在 Base 内部铸造新的长期存活的主体。
这正是此处失效的具体边界。
## 根本原因
该漏洞源于共享 Base 访问被集成到正常 ACL 路径的方式。
在共享 Base 的前端流程中,注入了 `xc-shared-base-id` 的同时移除了普通的 auth 标头。
然后,在后端,`BaseViewStrategy` 接受 `xc-shared-base-id` 并将共享链接直接转换为从共享 Base 配置派生出的普通 `roles` / `base_roles`。
这是第一个问题。
第二个问题是,**viewer 级别的权限依然包含成员管理操作**。
在 ACL 层中,`ProjectRoles.VIEWER` 可以触达:
- `baseUserList`
- `userInvite`
这些权限保护着正常的 meta 路由:
- `GET /api/v2/meta/bases/:baseId/users`
- `POST /api/v2/meta/bases/:baseId/users`
因此,公共共享会话实际上被允许访问旨在供真实 Base 参与者使用的成员身份接口。
最后一步在于邀请流程本身。
`BaseUsersService.userInvite()` 检查了角色权限,然后创建了:
- 一个带有 `invite_token` 的真实用户行
- 一个针对目标 Base 的真实 Base 成员行
而对于共享 Base 会话:
- `invited_by` 变成了 `null`
因为请求背后没有真实的已认证邀请者身份。
这就是整个 bug 链。
### 为什么这是可利用的
因为仅仅拥有共享 Base 链接就足够了。
攻击者不需要:
- `xc-auth`
- 预先存在的账户
- 被盗的凭据
- 或 Base 中先前的成员身份
利用链非常直接:
- 攻击者获取共享 Base UUID
- 该 UUID 被接受为 Base 查看者主体
- 查看者 ACL 触达成员管理接口
- 攻击者列出当前的 Base 成员
- 攻击者邀请任意电子邮件地址
- 被邀请的用户通过正常的注册流程兑换 token
- 新账户成为真实的已认证 Base 成员
- 所有者随后禁用共享链接
- 被邀请的账户依然保留正常的已认证访问权限
这将可撤销的链接共享转变成了持久的成员身份。
## 为什么这是一个安全问题,而不仅仅是奇怪的共享行为
重要的区别在于撤销后的持久性。
这不仅仅是:
漏洞主体不是普通的已认证查看者。
它是一个**公共共享会话**。
这很重要,因为应用程序将一个临时的、限定范围的链接主体视为足够受信任,以至于可以:
- 枚举真实成员
- 更改访问控制状态
- 并在 Base 内部创建新的持久主体
真正的问题不在于:
真正的问题在于:
答案是肯定的。
这就是为什么这是一个真正的授权漏洞,而不仅仅是一个令人惊讶的应用程序行为。
## PoC
我在本地针对以下环境进行了验证:
- 产品版本:`0.301.3`
- commit:`dac49b0122c5ee655fb8f46a1b6e42dfeec1f3ad`
- Base URL:`http://127.0.0.1:8080`
重现过程非常直接。
首先,我作为一个普通的所有者账户登录,创建了一个全新的 Base,创建了一个表格,并以 `viewer` 的身份启用了共享 Base 访问:
```
PATCH /api/v2/meta/bases/标签:MITM代理, NocoDB, 协议分析, 开源漏洞披露, 权限提升, 访问控制漏洞