文章总结: GeckoAI安全团队在Cal.com中发现关键链式漏洞,包括通过组织邀请令牌实现的完全账户接管和绕过授权访问所有预订数据的IDOR漏洞。账户接管源于三个链式错误:用户名验证跳过组织成员检查、组织范围验证遗漏跨组织用户、全局唯一邮箱的upsert操作覆盖受害者凭据。API端点中下划线前缀的内部路由文件被Next.js直接暴露,导致认证绕过。漏洞允许攻击者读取/修改数百万条预订信息及参会者PII数据。Cal.com已在v6.0.8中修复,强调纵深防御对防范链式攻击的重要性。 综合评分: 88 文章分类: 漏洞分析,Web安全,渗透测试,安全工具,AI安全
缺失的访问控制如何导致 Cal.com 泄露数百万条预订信息并导致完全账户接管
Jeevan Jutla Jeevan Jutla
securitainment
2026年2月8日 22:25 中国香港
| 原文链接 | 作者 | | — | — | | https://www.gecko.security/blog/caldotcom-broken-access-controls | Jeevan Jutla |
执行摘要
Cal.com 是一个开源的日程调度基础设施,为 Calendly 提供了一个开发者友好的替代方案。它提供了可自托管或云托管的平台,具备日历同步、可用性管理、团队调度、内置视频会议以及用于将预订体验嵌入到任何应用程序中的 API。
Gecko 的 AI 安全工程师在 Cal.com Cloud 中发现了一系列关键且影响重大的链式漏洞,这些漏洞允许攻击者对任何 Cal.com 用户执行完全账户接管,并读取或修改任何预订,包括带有参会者元数据的私人会议。
在整个漏洞发现过程中,Gecko 自主识别了所有发现,使我们能够在数小时内发现复杂的多步骤漏洞利用链。这些漏洞原本可能需要数周才能发现,且已经绕过了 Cal.com 现有工具和人工渗透测试。
这正是我们在 Gecko 所努力的方向:普及 AI 增强的安全专业知识,并将其转化为每个开发者和安全团队都可以使用的能力。我们正在构建基础设施、工具、代理和工作流程,以捕获安全知识并通过 LLM 进行放大,将这种能力直接交到人们手中,以大规模保护软件安全。
本文的其余部分将详细介绍漏洞利用链,以及 AI 如何处理不熟悉的代码库并发现和验证关键发现。
我们要感谢 Cal.com 安全团队的快速响应和合作,以解决这些问题。在这些漏洞被报告后,它们在几天内就被修补并推送上线。
Gecko 的 AI SAST:研究背后的工具
在深入探讨漏洞之前,让我们先谈谈使这项研究成为可能的工具。
Gecko 是一个 AI 驱动的静态分析平台,其处理代码安全的方式与传统的 SAST 工具不同。它不是根据预定义的规则进行模式匹配,而是使用语言服务器构建代码库的语义索引,就像 IDE 理解代码一样。这为我们提供了跨文件和仓库的编译器级精确符号解析,这意味着我们可以追踪复杂的调用链,并以基于 AST 的扫描器无法做到的方式推理业务逻辑。
在索引过程中,Gecko 识别端点,映射认证和授权机制,并构建数据如何在应用程序中流动的图谱。这使我们能够生成准确的漏洞利用概念验证,跨微服务进行扫描,并识别关键代码路径上缺失的授权。
寻找损坏的访问控制
损坏的访问控制漏洞几乎存在于每个应用程序中。正如 OWASP 在 2025 Top 10 列表中指出的:
“在 Top Ten 中保持第 1 名的位置,100% 的被测应用程序都被发现存在某种形式的损坏访问控制。”
Cal.com 的使命是在 2031 年前连接 10 亿用户,而开源是其理念的重要组成部分。他们拥有近 1,000 名贡献者,并且非常注重安全性,这使他们成为验证 OWASP 针对安全意识代码库声明的合适目标。
通过组织邀请令牌进行账户接管
最关键的发现是注册流程中的一个认证绕过漏洞,允许攻击者利用组织团队邀请令牌来接管现有用户账户。当用户已经是某个组织的成员时,用户名验证逻辑错误地返回 available: true,允许注册流程继续并覆盖受害者的密码。这赋予了攻击者对其账户的完全访问权限。
该漏洞源于三个链式错误:
- 用户名验证错误地批准了已存在于组织中的用户的注册。
- 电子邮件验证仅在攻击者的组织范围内进行检查,遗漏了其他组织中的受害者。
- 数据库 upsert 操作使用全局唯一的电子邮件地址来匹配用户,导致它覆盖了受害者的凭据。
这一链条使任何拥有组织的攻击者都能够通过简单地知道其电子邮件地址来接管不同组织中用户的账户。
1. 用户名注册绕过组织成员
usernameCheckForSignup函数通过电子邮件查找现有用户,但仅在他们不是组织成员时才验证可用性。
packages/lib/server/username.ts#L239-L286
const usernameCheckForSignup =async ({ username, email }) => {
const response = {
available: true, // Default assumes email is available
premium: false,
suggestedUsername: "",
};
const username =slugify(usernameRaw);
// Find existing user by email (global search)
const user =awaitprisma.user.findUnique({
where: { email },
select: { id: true, username: true, organizationId: true },
});
if (user) {
// Check if user belongs to any organization
const userIsAMemberOfAnOrg =awaitprisma.membership.findFirst({
where: {
userId: user.id,
team: { isOrganization: true },
},
});
// Vulnerability 1: Only validates if user is NOT in an org
if (!userIsAMemberOfAnOrg) {
// This validation only runs for non-org users
const isClaimingAlreadySetUsername =user.username===username;
const isClaimingUnsetUsername =!user.username;
response.available=isClaimingUnsetUsername||isClaimingAlreadySetUsername;
response.premium=awaitisPremiumUserName(username);
}
// If userIsAMemberOfAnOrg is true, response.available stays TRUE
// This allows org members to be "re-signed up" by attackers
}
returnresponse; // Returns { available: true } for org members
};
当用户属于任何组织时,验证逻辑被完全跳过,使 available标志保持在其默认值 true。这错误地表明该电子邮件可用于注册,即使存在活跃账户,也允许流程继续。该函数应该拒绝所有现有已验证用户的注册,无论其组织成员身份如何,但它反而为最容易受到跨组织攻击的用户创建了一个危险的例外。
2. 组织范围验证不检查其他组织
第二次验证仅在目标组织范围内搜索现有用户。
packages/features/auth/signup/utils/validateUsername.ts#L45-L68
const existingUser =awaitprisma.user.findFirst({
where: {
// Vulnerability 2: Only searches within the target organization
...(organizationId? { organizationId } : {}), // WHERE organizationId = attacker's org
OR: [
// Skip username check in org context
...(!organizationId? [{ username }] : [{}]),
{
AND: [
{ email }, // Check for this email
{
OR: [
{ emailVerified: { not: null } }, // Email is verified
{ AND: [{ password: { isNot: null } }, { username: { not: null } }] },
],
},
],
},
],
},
select: { email: true },
});
// This translates to SQL:
// SELECT email FROM User
// WHERE organizationId = <attacker_org_id> ← Only checks attacker's org
// AND email = '[email protected]'
// AND emailVerified IS NOT NULL
// If victim is in a different org, query returns NULL
return { isValid: !existingUser }; // Returns true = email "available"
这转换为带有 WHERE organizationId = <attacker_org_id>的 SQL 查询。当受害者属于不同的组织时,此范围查询不返回任何结果,导致验证器错误地得出电子邮件可用的结论。该函数问的是”这个电子邮件在我的组织中是否存在?”而它应该问的是”这个电子邮件是否作为已验证用户存在于任何地方?”
3. 全局电子邮件 Upsert 覆盖受害者
在两次验证都错误地通过之后,处理程序执行一个带有 where: { email }的 prisma.user.upsert()操作。
packages/features/auth/signup/handlers/calcomHandler.ts#L170-L182
if (foundToken&&foundToken?.teamId) {
const team =awaitprisma.team.findUnique({
where: { id: foundToken.teamId },
include: {
parent: { select: { id: true, slug: true, organizationSettings: true } },
organizationSettings: true,
},
});
if (team) {
const organizationId =team.isOrganization?team.id:team.parent?.id??null;
// Vulnerability 3: Email is globally unique, so this finds any user with this email
constuser=awaitprisma.user.upsert({
where: { email }, // Matches victim's email across all orgs
update: {
username, // Changes username
emailVerified: newDate(Date.now()),
identityProvider: IdentityProvider.CAL,
password: {
upsert: {
create: { hash: hashedPassword },
update: { hash: hashedPassword }, // Overwrites victim's password
},
},
organizationId, // Moves victim to attacker's org
},
create: {
// This block won't execute, victim already exists
username,
email,
identityProvider: IdentityProvider.CAL,
password: { create: { hash: hashedPassword } },
organizationId,
},
});
// Victim is now locked out, attacker has full access
}
}
由于电子邮件地址在数据库模式中是全局唯一的,因此此子句无论组织如何都会匹配受害者的现有用户记录。然后执行 update 块,用攻击者选择的密码覆盖受害者的密码哈希,并将其 organizationId更改为攻击者的组织。受害者立即被锁定在其账户之外,攻击者获得完全访问权限。受害者的所有数据,包括日历集成、OAuth 令牌、预订和 API 密钥,都变得对攻击者可访问。
攻击流程
漏洞利用过程非常简单。攻击者为其拥有的组织生成一个可共享的邀请链接,生成一个类似 https://app.cal.com/signup?token=<64-char-hex-token>的 URL。他们访问该 URL 并使用任意受害者的电子邮件和新密码填写注册表单。注册成功,攻击者现在实现了完全账户接管。受害者的原始密码不再有效。不会向受害者发送任何通知。
Cal.com 在 v6.0.8 中修复了此问题,方法是在使用邀请令牌注册之前添加用户存在验证。
Cal.com 账户接管修复
Cal.com 预订端点中的损坏访问控制
第二个漏洞通过两个基本缺陷暴露了所有预订数据和用户记录:端点上的缺失访问控制和不安全的直接对象引用(IDOR)。
在索引过程中,Gecko 通过识别端点等功能并为其分配属性(包括请求路径、HTTP 方法和认证机制)来增强其索引的上下文信息。这使我们能够将所有生产者和消费者及其关系一起映射,这意味着我们可以用顺序 curl 命令生成准确的概念验证,并识别关键端点上缺失的认证。
在此阶段,Gecko 在 API v1 中识别出四个暴露的端点,这些端点使用下划线前缀文件( _get.ts、 _post.ts、 _patch.ts、 _delete.ts)作为内部路由处理器。主要的 index.ts入口点在调用这些处理器之前正确地应用了授权中间件。然而,Next.js 将这些下划线文件暴露为可直接访问的路由。直接访问这些路由会绕过所有授权检查。
Cal.com 预订端点图谱展示 /bookings/_get
Cal.com 预订端点图谱展示 /bookings/[id]/_delete
这允许任何拥有有效 v1 API 密钥的认证用户读取和删除整个平台上的所有预订,暴露了:
- 参会者的电子邮件、姓名和个人详细信息
- 会议元数据和日历详细信息
- 其他用户和组织的完整预订历史
相同的模式影响了目标日历端点,允许任何认证用户通过 ID 删除任何用户的目标日历,静默地破坏日历路由规则。
Cal.com 的修复方案更新了 Next.js 中间件,以阻止对内部路由处理器( /_get、 /_post、 /_patch、 /_delete、 /_auth-middleware)的直接访问,对任何尝试直接访问这些路径的请求返回 403 Forbidden。
结论与关键要点
这项研究强调了损坏的访问控制问题如何几乎存在于每个应用程序中,以及核心组件中几个微妙的错误如何链接在一起摧毁安全边界。
这些漏洞的影响导致了 Cal.com 上任何用户的完全账户接管,包括管理员账户和付费用户,并暴露了所有敏感的预订数据,包括 PII。
纵深防御至关重要。虽然每个单独的漏洞在单独看来可能微不足道,但将它们链接在一起产生了重大影响。这充分说明了分层安全的重要性。
现在的目标是将这种相同的速度普及给每个人。Gecko 希望将这种自动化、AI 辅助的检测和验证带入安全团队的工具包中,以便防御者可以在攻击者将它们拼凑在一起之前发现和修复复杂的链式问题。
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment Jeevan Jutla Jeevan Jutla《缺失的访问控制如何导致 Cal.com 泄露数百万条预订信息并导致完全账户接管》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。








评论