79% 对 51%:独立数字银行的应用安全落后。 阅读报告

深度解析

深度解析

Seafile SeaDoc:通过公开分享链接实现 RCE(CVE-2026-129167)

匿名 SeaDoc 分享令牌可到达转换器的所有端点,绝对路径会植入并执行一个 Python 包;已在 2.0.11 和 3.0.8 修复。

TL;DR

  • SeaDoc 的 Confluence 导入中的绝对路径让公开 .sdoc 分享链接的匿名持有者把 Python 包写入转换器的库路径并执行。Haiwen 于 2026 年 11 月 2 日修复了服务器。
  • SeaDoc 2.0.4 至 2.0.10 和 3.0.0 至 3.0.7 受影响。实例必须有一个公开 .sdoc 分享链接或已发布的 wiki。
  • CVE 记录将其评为 CVSS v4.0 9.2 Critical。没有报告利用,截至 2026 年 11 月 14 日也不在 CISA 的 Known Exploited Vulnerabilities(KEV)目录中。
  • 升级到 SeaDoc 2.0.11 或 3.0.8,或更高版本。转换器提交和缓解措施见“如何修复?”。

我受影响吗?

以下条件全部成立时,您会受影响:

  • 部署运行 SeaDoc 2.0.4 至 2.0.10,或 3.0.0 至 3.0.7。
  • 实例有一个公开 .sdoc 分享链接或已发布的 wiki。分享页面嵌入的匿名令牌就是攻击使用的凭据。
  • SeaDoc 转换器可通过 Seafile 前端访问。官方 Docker Compose 部署将其暴露在 /sdoc-server/converter/ 下。
  • 部署是在 2026 年 11 月 2 日之前完成的,Haiwen 称当天修复了闭源 sdoc-server 组件。

没有公开 .sdoc 分享链接也没有已发布 wiki 的部署不会暴露本文描述的匿名令牌路径。能获取 SeaDoc 令牌的已登录用户仍可到达转换器。转换器接受任何有效的 SeaDoc 令牌,因此为一个文档签发的令牌可到达转换器的所有端点。请把前提条件视为攻击路径的条件,而不是安全的默认状态。

分支 受影响版本 修复版本
SeaDoc 2.0 2.0.4 至 2.0.10 2.0.11
SeaDoc 3.0 3.0.0 至 3.0.7 3.0.8

撰写时的当前镜像为 2.0.13 和 3.0.9。两者都包含修复。

检查正在运行的镜像和公开分享:

# Image tag of the running SeaDoc container
docker inspect --format '{{.Config.Image}}' <seadoc-container>

# Images present on the host
docker image ls | grep sdoc-server

# Public links, in the Seafile web interface
# Open each library, then Sharing, and review the links that are public

对于官方部署,镜像标签是可靠的版本检查。compose 文件可能固定 2.0-latest 或 3.0-latest 等标签,这些标签可能在运行容器版本不变的情况下移动。

如何修复?

将 SeaDoc 升级到 2.0.11 或 3.0.8,或当前的 2.0.13 或 3.0.9,并重新创建容器。

  1. 拉取修复后的镜像并更新 compose 文件中的标签。
  2. 重新创建 seadoc 和 sdoc-server 容器。只拉取镜像而不重新创建容器会让旧进程继续运行。
  3. 使用 docker inspect 确认新标签。
  4. 检查公开 .sdoc 链接和已发布 wiki,删除不需要的。
  5. 如果无法升级,在反向代理处阻止转换器端点,或删除所有公开 .sdoc 链接并取消发布所有 wiki。缓解措施移除了匿名令牌路径,但不修复路径遍历。

开源 seadoc-converter 的修复是 2026 年 10 月 29 日的提交 cb43162e。它拒绝不是字符串、不以 .html.zip 结尾或包含 /、\ 或 NUL 字节的 filename。它还拒绝空的 space_key、. 和 ..。

sdoc-server 组件是闭源的。Haiwen 于 2026 年 11 月 2 日通过电子邮件告诉研究者 SeaDoc 2.0 和 3.0 已修复。没有公开提交或公告描述这一半修复。

根本原因是什么?

转换器只检查 SeaDoc JWT 的签名就接受任何令牌,然后把攻击者控制的文件名用作 ZIP 解压的绝对路径,并在上传挂起期间保留已解压的文件。

SeaDoc 有三个进程:seahub 负责认证和页面渲染,sdoc-server 负责协同编辑,seadoc-converter 负责文档转换。三者共享 HMAC 密钥 JWT_PRIVATE_KEY。访问者打开公开 .sdoc 分享链接时,seahub 会在页面中嵌入 JWT,以便匿名预览。

转换器的 check_auth_token() 用 config.SEADOC_PRIVATE_KEY 验证签名并返回 true。它从不检查令牌签发给哪个文档,也不检查令牌携带的权限:

def check_auth_token(req):
    auth = req.headers.get('Authorization', '').split()
    if not auth or auth[0].lower() != 'token' or len(auth) != 2:
        return False

    token = auth[1]
    if not token:
        return False

    private_key = config.SEADOC_PRIVATE_KEY
    try:
        jwt.decode(token, private_key, algorithms=['HS256'])
    except (jwt.ExpiredSignatureError, jwt.InvalidSignatureError) as e:
        logger.exception(e)
        return False

    return True

匿名令牌是只读的,但转换器忽略 permission 字段。因此令牌可到达 /api/v1/confluence-to-wiki/,该端点接收 filename 并用 os.path.join 构造路径:

    underscore_index = filename.rfind('_')
    if underscore_index != -1:
        space_key = filename[underscore_index + 1:-len('.html.zip')]
    elif 'Confluence-space-export-' in filename:
        wiki_name = filename[:-len('.html.zip')]
        space_key = filename[len('Confluence-space-export-'):-len('.html.zip')]
    else:
        wiki_name = filename[:-len('.html.zip')]
        space_key = wiki_name
    try:
        extract_dir = '/tmp/wiki'
        space_dir = os.path.join(extract_dir, space_key)
        zip_file_path = os.path.join(extract_dir, filename)
        if not os.path.exists(extract_dir):
            os.mkdir(extract_dir)
        # Deploy on two machines
        is_same_machine = True
        if not os.path.exists(space_dir):
            os.mkdir(space_dir)
            is_same_machine = False
            response = requests.get(download_url)
            with ZipFile(BytesIO(response.content), 'r') as zip_ref:
                zip_ref.extractall(space_dir)

当 os.path.join 的第二个参数以斜杠开头时,Python 会丢弃第一个参数。像 /usr/lib/python3.12/encodings/cp273 这样的 space_key 会让 space_dir 和 zip_file_path 变成绝对路径。转换器创建该目录并把攻击者的 ZIP 解压到那里。

清理只在 process_zip_file() 返回后于 finally 块中运行。该函数把结果发送到 upload_url,没有超时。接受连接但永不响应的上传目标会让请求一直阻塞,因此 finally 块不运行,植入的文件留在磁盘上。

如何被利用?

  1. 攻击者打开公开 .sdoc 分享链接,从页面源码读取 seadocAccessToken 的值。该令牌是该文档的有效匿名凭据。
  2. 攻击者运行一个服务器,返回包含 __init__.py 的 ZIP,接受连接但永不响应,并在被请求时返回带 charset=cp273 的 HTTP 500。
  3. 攻击者向 /sdoc-server/converter/api/v1/confluence-to-wiki/ 发送 POST,使用绝对 filename(如 /usr/lib/python3.12/encodings/cp273aaaaaaaaa)、提供 ZIP 的 download_url 和会挂起的 upload_url。转换器把 ZIP 解压到 Python 的 encodings 包中。
  4. 攻击者向 /sdoc-server/converter/api/v1/file-convert/ 发送 POST,upload_url 返回 500 和 charset=cp273。转换器记录响应正文,Response.text 用声明的 charset 解码它。
  5. CPython 延迟导入 encodings.cp273。FileFinder 先检查目录包 cp273/__init__.py,再检查模块 cp273.py,因此植入的文件在转换器进程内运行。

研究者的公开分析包含两个请求和攻击者服务器。第一个请求在文件落盘期间阻塞,第二个请求触发导入。分析中的标记命令从转换器容器写入了 uid=0 (root)。

该路径不需要重启,因为导入在运行中的进程内按需发生。唯一前提是提供匿名令牌的公开 .sdoc 分享链接或已发布 wiki。

攻击如何展开?

匿名分享令牌通过仅验证签名的检查,绝对文件名把 Python 包写入转换器的库路径,charset 错误让 CPython 导入并运行它。

flowchart LR A["公开 .sdoc 分享页面
嵌入匿名 JWT"] --> B["转换器只检查
JWT 签名"] B --> C["confluence-to-wiki
在所有端点接受令牌"] C --> D["绝对 filename 让
os.path.join 写出 /tmp/wiki"] D --> E["ZIP 被解压到
Python encodings 包"] E --> F["挂起的上传阻止
finally 清理"] F --> G["charset=cp273 让 CPython
导入植入的包"] G --> H["代码在转换器
进程中运行"]

攻击者能做什么?

持有公开分享令牌的未认证攻击者可以在 SeaDoc 转换器进程中运行代码,官方容器以 root 运行该进程。

影响包括:

  • 读取和修改转换器能到达的文档,包括其配置中的共享 HMAC 密钥。
  • 使用该密钥为其他文档和服务伪造 SeaDoc 令牌。
  • 到达转换器容器可访问的网络和挂载卷。
  • 把转换器作为进入 Seafile 部署的立足点。

这些条件是影响的一部分:

  • 实例必须暴露公开 .sdoc 分享链接或已发布 wiki。
  • 转换器必须可从攻击者网络访问。
  • 部署必须是 2026 年 11 月 2 日之前构建的受影响 SeaDoc 版本。
  • 攻击者不需要账户,也不需要用户交互。

研究者在官方 Docker Compose 实验中演示了 uid=0 (root)。代码在转换器容器中运行,默认不在 Seafile 应用服务器上。能否到达主机取决于容器的挂载和网络。

为什么修复有效?

转换器修复拒绝任何可能逃出 /tmp/wiki 的文件名,因此 os.path.join 不再能产生绝对解压路径。

修复提交 cb43162e 修改了 confluence_to_wiki() 开头的校验,并在派生 space_key 后增加检查:

-    if not filename:
+    if (not isinstance(filename, str)
+            or not filename.endswith('.html.zip')
+            or any(char in filename for char in ('/', '\\', '\x00'))):
         return {'error_msg': 'path invalid.'}, 400
...
     else:
         wiki_name = filename[:-len('.html.zip')]
         space_key = wiki_name
+
+    if space_key in ('', '.', '..'):
+        return {'error_msg': 'path invalid.'}, 400

     try:
         extract_dir = '/tmp/wiki'
         space_dir = os.path.join(extract_dir, space_key)

该检查拒绝 filename 中的斜杠、反斜杠和 NUL 字节,因此派生出的每个 space_key 都是单一路径组件。space_key 检查拒绝仍会指向普通子路径之外的三个值。解压保持在 /tmp/wiki 下。

这是开源转换器中的部分修复。阻止匿名只读令牌到达转换器所有端点的闭源 sdoc-server 更改没有公开。Haiwen 称该组件已修复;令牌范围更改无法审查。

如何检查是否已遭入侵?

没有公开的厂商入侵指标。检查针对两个转换器请求和植入 Python 库路径的文件。

  • 在反向代理和 Seafile 访问日志中搜索发往 /sdoc-server/converter/api/v1/confluence-to-wiki/ 且 filename 以 / 开头或包含 .. 的 POST 请求。
  • 搜索发往 /sdoc-server/converter/api/v1/file-convert/ 且 upload_url 指向您基础设施之外的 POST 请求。
  • 在转换器容器中,检查 Python encodings 目录是否有您未安装的包,尤其是以 cp273 等编解码器命名的目录。
  • 检查 /tmp/wiki 中遗留的目录和 ZIP 文件。
  • 检查转换器容器的 /root、/tmp 和应用目录中是否有意外文件。

研究者的概念验证写入 /root/pwned;攻击者可以写入其他内容。在解压前失败的请求可能不留痕迹,转换器只记录上传响应正文,不记录完整请求。

时间线

  • 2026-10-24:研究者发布分析。
  • 2026-10-29:编写 seadoc-converter 修复提交 cb43162e。
  • 2026-11-02:Haiwen 告诉研究者闭源 sdoc-server 组件已修复。
  • 2026-11-13:CVE-2026-129167 于 17:21 UTC 发布。

从分析到 CVE 用时 20 天
图 1:分析于 2026 年 10 月 24 日发布,转换器修复于 10 月 29 日跟进。厂商于 11 月 2 日修复闭源组件,CVE 于 11 月 13 日发布。来源:研究者分析、修复提交和 CVE 记录,2026 年 11 月 14 日检查

常见问题

CVE-2026-129167 是否已在野利用?

没有报告利用。截至 2026 年 11 月 14 日,它不在 CISA 的 KEV 中,研究者的分析描述的是实验室复现,不是活动。

利用它需要账户吗?

不需要。攻击者需要公开 .sdoc 分享链接或已发布 wiki,并读取分享页面嵌入的匿名令牌。不需要 Seafile 账户。

匿名令牌会把攻击限制在一个文档吗?

不会。转换器只检查令牌签名。permission 和文档字段被忽略,因此匿名令牌可到达转换器的所有端点。

是 Seafile 服务器本身有漏洞,还是只有 SeaDoc?

缺陷在 SeaDoc 转换器和 SeaDoc 服务共享的令牌检查中。没有 SeaDoc 的 Seafile 部署不受此 CVE 影响。运行攻击者代码的是转换器容器。

哪个版本修复了它?

SeaDoc 2.0.11 和 3.0.8 是首批修复版本。当前的 2.0.13 和 3.0.9 也包含修复。闭源 sdoc-server 组件于 2026 年 11 月 2 日修复。

修复完整吗?

开源转换器的修复关闭了绝对路径遍历。闭源 sdoc-server 中的令牌范围更改没有公开,因此无法从代码审查其完整性。

参考文献

  • CVE-2026-129167 记录,VulnCheck CNA,2026-11-13 发布,CVSS v4.0 9.2:https://www.cve.org/CVERecord?id=CVE-2026-129167(2026 年 11 月 14 日检查)
  • Wu Yulun,“Seafile Vulnerability Report”,2026-10-24 发布,根本原因、复现和两个请求:https://vul.yulun.ac.cn/20261024/seafile/(2026 年 11 月 14 日检查)
  • seadoc-converter 修复提交 cb43162e,2026 年 10 月 29 日,文件名和 space_key 校验:https://github.com/haiwen/seadoc-converter/commit/cb43162e8bc2991a2900c8acf85090358a30e516(2026 年 11 月 14 日检查)
  • 易受攻击的 seadoc-converter 源码 bb6cf0f,seadoc_converter/server/apis.py,check_auth_token() 和 confluence_to_wiki():https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/server/apis.py(2026 年 11 月 14 日检查)
  • 易受攻击的 seadoc-converter 源码 bb6cf0f,seadoc_converter/converter/utils.py,无超时的上传:https://github.com/haiwen/seadoc-converter/blob/bb6cf0fb9ebc6bb05c9161acbfd6289737ec2cd6/seadoc_converter/converter/utils.py(2026 年 11 月 14 日检查)
  • Docker Hub,seafileltd/sdoc-server 标签,2.0.11、3.0.8、2.0.13 和 3.0.9:https://hub.docker.com/r/seafileltd/sdoc-server/tags(2026 年 11 月 14 日检查)
  • Docker Hub,seafileltd/seafile-mc 拉取次数,20,336,673:https://hub.docker.com/r/seafileltd/seafile-mc(2026 年 11 月 14 日检查)
  • CISA Known Exploited Vulnerabilities 目录,2026 年 11 月 14 日检查 CVE-2026-129167(未列出):https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json(2026 年 11 月 14 日检查)