一个库、八款产品、一个请求直达您的配置文件
无需登录,照样返回了结果。在接入 Atlassian Crowd 的实例上,这意味着一个完全未经身份认证的请求,换回了一个携带密码的 200 OK。2026 年 10 月 5 日,Atlassian 为 CVE-2026-21589 发布了紧急修复。这是一个未授权的任意文件读取漏洞,CVSS 4.0 评分为 9.3。[1] 同一个缺陷波及八款 Data Center 和 Server 产品:Jira Software、Jira Service Management、Confluence、Bitbucket、Bamboo、Crowd、Crucible 和 Fisheye。它们并非巧合地同时出问题,而是共用了一个库——atlassian-plugins-webresource,漏洞就藏在这个库里。安全公告列出了每款产品的修复版本;该库本身在 6.0.8 中得到修复。[1][2]
Atlassian 对影响范围的描述很谨慎:攻击者可以读取“Web 应用根目录下的特定文件”,而且前提是已经知道确切路径,无法列出目录。[1] 听起来范围很窄。其实不然,而说清楚为什么不窄,正是本文的重点。我没有在真实实例上复现这个漏洞;以下内容基于 Atlassian 的安全公告和 watchTowr 的根因分析,查阅时间为 2026 年 10 月 8 日。[1][2]
// atlassian-plugins-webresource, before 6.0.8
escapeSlashes(String string) { return string.replaceAll("/", "::"); }
unescapeSlashes(String string) { return string.replaceAll("::", "/"); }
// How a web-resource name is handled on the way in:
resourceName // attacker-controlled
-> check for ".." / "/" traversal // sees "..::", no slash -> looks safe
-> unescapeSlashes(resourceName) // "..::" becomes "../"
-> load file relative to web root // traversal now active服务器以为 :: 是安全的,而攻击者控制着它最终变成什么
开发者信任的是这样一种设计:Web 资源的 URL 包含资源键和资源名。资源名中的斜杠会让路由器混淆,所以库会对它进行转义:每个“/”在进入时都变成“::”,之后每个“::”又会还原为“/”。[2] 这样的编码方式本身是合理的。漏洞不在编码,而在顺序。
代码首先检查传入的字符串中是否存在路径遍历,而此时斜杠仍被写成“::”。对于这项检查来说,“..::”这样的序列不含任何斜杠,看起来就像一个普通文件名,于是顺利通过。直到之后,库才对字符串进行反转义,“..::”于是变成了“../”。[2] 判定请求安全的组件和实际访问文件系统的组件,看到的是两个不同的字符串。二者之间的空隙,就被攻击者牢牢掌控。
为什么一次文件读取,实际上等于接管了您的身份层
我们一步步来看。经过转义、看似要做路径遍历的资源名通过了校验,被解码回真正的“../”片段,然后交给从 classpath 和 Web 应用根目录读取文件的加载器。[2] watchTowr 报告称,读取范围被限制在 Tomcat 上下文之内,因此无法一路跳到 /etc/passwd。但对攻击者来说,这个边界几乎无关紧要,因为真正值得读取的文件本来就在里面。
最需要关注的是 WEB-INF/classes/crowd.properties。在接入 Atlassian Crowd 的产品上,这个文件保存着与身份服务器通信所用的应用名称和应用密码。[2] 一个未授权的“读取文件”漏洞,就这样交出了管理您所有用户的系统的凭据。watchTowr 详细演示了这一步:读取该文件,拿到 Crowd 应用密码,攻击者就不再只是读文件,而是以受信任应用的身份与身份层直接对话。[2]
这正是新闻标题所忽略的关键一步。“读取特定文件”与“把您的身份凭据交给攻击者”其实是同一个漏洞,区别仅在于是否知道该请求哪个文件。公告中提到的前提——必须知道确切路径——确实存在,但 crowd.properties 并不是什么秘密位置,这个文件一直就放在那里。[1][2]
# WEB-INF/classes/crowd.properties (illustrative keys, not real values)
application.name = my-confluence
application.password = <shared secret to the Crowd identity server>
crowd.server.url = https://id.example.internal/crowd/services/
crowd.base.url = https://id.example.internal/crowd/补丁调换了两个步骤的顺序,请确认您部署的正是这次调换
atlassian-plugins-webresource 6.0.8 中的修复,让检查和文件系统看到同一个字符串,从而堵上了这个空隙:资源名在路径遍历检查之前、而不是之后,就被解码为真实形式。[2] 这条原则值得原样记住:先规范化,再校验。只有当安全检查针对的正是最终到达危险操作的那个值时,检查才有意义。下面的流程展示了修正后的顺序,用于辅助审计,并非 Atlassian 补丁差异的原文。
对于产品负责人来说,这里的陷阱是相信版本号,而不是实际运行的制品。其中好几款产品都发布了不止一个修复分支;一个回滚镜像、一台预发布服务器,或一台被遗忘的 Crucible 服务器,都可能仍在加载 6.0.7 版本的库,而您的记录上却写着“已修补”。[1] 请检查实际可被访问的实例上,真正被加载的那个 jar 包。开发人员笔记本上打过补丁的代码副本,并不能证明面向互联网的那台服务器是安全的。
// The fix, as a principle (not Atlassian's exact diff):
// decode to the real value FIRST, then validate THAT value.
resourceName
-> unescapeSlashes(resourceName) // "..::" becomes "../" up front
-> check the decoded value for ".." traversal // now the check sees it
-> reject, or load file relative to web root如何在非 Atlassian 的代码中找到同样的模式
这个漏洞其实与 Atlassian 本身关系不大。它属于一类问题,这类问题就是“先校验,后转换”。只要代码在检查完一个值是否危险之后,又对它进行解码、反转义、规范化或标准化,就会存在同样的漏洞,无论最终的操作是文件读取、SSRF 还是 SQL 语句。我做代码审计时,首先用 grep 查找的,就是针对同一个变量、位于校验步骤之后的解码步骤。
下面的模式是我的起点。它能找出解码调用和危险操作调用,但真正的工作要靠人工完成。对于每一处命中,我只问一个问题:“..”检查所看到的字符串,与最终到达文件系统的字符串,是否逐字节完全相同?如果中间还有一步转换,那么检查所针对的值,在真正起作用的时刻已经不存在了。这个问题比任何扫描器特征都能更快地发现这类漏洞,因为扫描器匹配的是名称,而我比对的是顺序。
# Smell to hunt: a decode/normalise step that runs AFTER the path check,
# on the same variable. Start broad, then read each hit by hand.
rg -ni "replaceAll|URLDecoder|decode|unescape|normalize|canonical" \
--glob '!**/test/**'
# The one question that confirms the bug:
# is the string that reaches the file sink byte-for-byte the string
# the ".." check inspected? If a transform sits between them, it is vulnerable.这是您要救的火吗?对一部分人来说,是的,就在今天
先如实评估影响规模。受影响的只有自托管的 Data Center 和 Server 版本。Atlassian 表示,Cloud 实例已经打好补丁,且没有发现被利用的证据。[1] 如果您完全使用 Atlassian Cloud,这不是您的事件。如果您自托管了这八款产品中的任何一款,并将其暴露在互联网上,那就是您的事件,而且倒计时已经开始。
watchTowr 于 10 月 6 日公布了根本原因和概念验证,并估计暴露在外的实例数量“在六位数到七位数之间”,按其统计,仅 Confluence 实例就有近 70 万个。[2] 这个数字是他们的估计,不是我的。毫无争议的是速度。据 The Hacker News 和 BleepingComputer 报道,细节公开后约两小时,蜜罐就观测到了利用尝试,目前主要是系统指纹识别和配置文件扫描。[3][4] 10 月 18 日更新:CISA 已确认该漏洞正被积极利用,并于 10 月 15 日将其加入“已知被利用漏洞”目录,联邦机构的修补期限就是今天。[5]
因此,判断这是否是您的紧急事件,只需看三个条件:您自托管了这八款产品之一;它可以从互联网或您并不完全信任的网段访问;并且在 10 月 5 日以来的这段时间里,它运行的是未打补丁的库版本。如果三个条件都满足,打补丁只是第一步,而不是全部工作,因为一个曾经可以被读取的文件,可能已经被读走了。
在关闭这个页面之前该做什么
下面的清单就是我会遵循的工作顺序。首先打补丁,因为这里的缓解窗口要以小时计,而不是以周计。[3] 但如果您是自托管且曾经暴露在外,请假定 Crowd 应用密码以及 Web 根目录下的其他任何内容都可能已被读取,并立即轮换。补丁能阻止下一次读取,却无法收回已经泄露出去的密钥。
访问日志能告诉您是否有人尝试过。需要排查的特征是:补丁之前对 Web 资源路由发起、且资源名中带有“::”“%3a%3a”或“..”的请求。命中并不能证明读取一定成功,但它能告诉您哪些实例应被视为可疑,以及应当优先轮换哪些密钥。
Patch and verify (in this order):
[ ] List every self-hosted install: Jira, JSM, Confluence, Bitbucket,
Bamboo, Crowd, Crucible, Fisheye.
[ ] Confirm the build that is RUNNING, not the one in your notes.
[ ] Patch each product to its fixed version from the advisory [1].
[ ] Cannot patch yet? Apply Atlassian's WAF / RewriteValve mitigation [1].
If you were exposed and unpatched, assume read, then rotate:
[ ] The Crowd application password in crowd.properties.
[ ] Any other secret stored under the web application root.
Hunt the access logs for pre-patch probing:
grep -iE "/(s|resources|sources)/[^ ]*(::|%3a%3a|\.\.)" access.log版本升级不会告诉您的那部分
CVE-2026-21589 是一个典型例子:仅凭版本号根本看不出这个漏洞。修复只是调换了两个内部步骤的顺序,而要确认您的部署是否安全,唯一的方法是检查正在运行的制品,以及通往文件系统的那条代码路径。这就是“我们已经打了补丁”与“我们已经验证真正关心的问题已被堵上”之间的差距。
把一个请求从边界开始,穿过框架,一直追踪到决定读取什么内容的那一行代码,这正是安全代码审计的核心;而这个 CVE 中“先校验、后转换”的问题模式,也是我在与 Atlassian 毫无关系的代码中同样会去查找的。如果您正在上线的产品会把外部输入转化为文件访问、数据库查询或内部请求,这正是我会帮您检查的边界。告诉我您正在运行什么,我们来确定范围。
参考来源
需要在您自己的产品中检查这个问题吗?
安全代码审计想学会自己发现这类漏洞吗?
安全代码审计课程(一对一)