作者:@x0root
漏洞:通过浏览器可渲染的上传文件(SVG / HTML)导致的存储型跨站脚本(XSS)
受影响软件:FileRise(< 2.7.1)
已修复版本:2.7.1
官方 CVE(通过 GHSA 申请):CVE-2025-68116(跟踪/公告:GHSA-35pp-ggh6-c59c)
先前相关公告(被绕过的最初缓解措施):GHSA-qrcv-vjvf-fr29
CVSS 评估:
报告者的评估在利用时而非植入漏洞时评估所需权限(PR)。
利用发生在受害者访问生成的公共分享链接时,该过程无需身份验证或任何权限(PR:N)。
CNA 的评估基于上传恶意文件的能力来评价 PR。然而,CVSS v3.1 将所需权限(PR)定义为攻击者在漏洞被利用时必须具备的权限,而不是为植入或准备漏洞条件所需的权限。
因此,PR:N 更准确地反映了现实世界的利用条件,从而得出严重(9.6)的严重性等级。
注意:GHSA-qrcv-vjvf-fr29 引入了一项缓解措施,阻止 SVG 在 FileRise Web UI(预览窗格)内渲染。本报告记录了该缓解措施的一种绕过方式——具体针对后端分享/下载端点——对应跟踪编号为 GHSA-35pp-ggh6-c59c / CVE-2025-68116。
本文档是 CVE-2025-68116 的完整技术记录:FileRise 中一种存储型 XSS,在早期缓解措施之后仍然存在,最终在 v2.7.1 中修复。内容包括发现过程、利用概念验证、多次失败的修复尝试、精确的根因控制流分析(附证据)、补丁的最终验证以及与 CVSS 评估相关的可利用性特征分析。以下所有内容均基于复现测试、控制器代码审查和公开公告线程。
先前的公告 GHSA-qrcv-vjvf-fr29 通过阻止 FileRise Web UI 中的内联渲染来处理通过 SVG 上传导致的存储型 XSS。该缓解措施没有解决后端端点如何提供 SVG 文件的问题,例如:
/api/file/download.php/api/file/share.phpCVE-2025-68116(跟踪编号为 GHSA-35pp-ggh6-c59c)记录了针对 GHSA-qrcv-vjvf-fr29 缓解措施的绕过:攻击者可以存储精心构造的 SVG,并通过公共分享链接或某些下载行为将其传递给受害者,导致脚本在 FileRise 源(origin)中执行。
为了验证后端是否仍以可渲染的方式暴露 SVG,我上传了一个简单的 PoC SVG:
通过以下方式访问该文件:
/api/file/download.php?…/api/file/share.php?token=…导致 alert() 执行。最初的 GHSA-qrcv-vjvf-fr29 缓解措施(UI 预览阻止)通过直接访问这些端点被绕过。
alert() 只是一个概念验证;我通过让负载与内部 API 交互来测试实际影响。
使用的测试负载:
<svg version="1.1" xmlns="http://www.w3.org/2000/svg">
<script type="text/javascript">
fetch('/api/upload/upload.php')
.then(response => response.text())
.then(data => alert('API Response: ' + data));
</script>
</svg>
当已登录的管理员打开包含此 SVG 的分享链接时,脚本执行并发起了已认证的 API 请求。观察到的效果包括:
{"csrf_expired":true,"csrf_token":"..."})测试期间展示的影响分类:
我私下报告了该问题。维护者发布了多个增量修复:
在 v2.6.0 → v2.7.0 期间,分享链接端点持续以允许内联渲染和脚本执行的方式提供 SVG。下面的根因分析解释了为什么早期修复未能完全封堵该攻击向量。
根本原因不是缺少某个标头,而是 shareFile()(控制器)内部的控制流和输出顺序问题,导致在许多执行路径上无法应用安全标头。存在两类问题:
exit; 点,在安全标头设置之前就短路了函数。我使用 awk 扫描来列出 shareFile() 内直到 readfile() 调用之前的 header() 和 exit; 出现位置:
命令:
awk '/function shareFile(/ {flag=1} /readfile(/ {flag=0} flag && /(header|exit;)/ {printf "%4d | %s\n", NR, $0}' src/controllers/FileController.php
观察到的输出(摘自运行结果,已删减):
1649 | header('Content-Type: application/json; charset=utf-8'); 1651 | exit; 1657 | header('Content-Type: application/json; charset=utf-8'); 1659 | exit; 1664 | header('Content-Type: application/json; charset=utf-8'); 1666 | exit; 1670 | header("Content-Type: text/html; charset=utf-8"); 1693 | exit; 1699 | header('Content-Type: application/json; charset=utf-8'); 1701 | exit; 1719 | header('Content-Type: application/json; charset=utf-8'); 1721 | exit; 1725 | header('Content-Type: application/json; charset=utf-8'); 1727 | exit;
安全标头(强化逻辑)从约第 1743 行开始:
1743 | header('X-Content-Type-Options: nosniff'); ... 1770 | header("Content-Disposition: attachment; ...");
由于该函数在许多路径中提前发出标头并 exit;,这些请求永远无法到达设置 Content-Disposition、nosniff 或限制性类型的强化代码。
在受密码保护的分享流程中,该函数提前输出了密码提示 HTML:
if (!empty($record['password']) && empty($providedPass)) { header("Content-Type: text/html; charset=utf-8"); ... exit; }
该路径发送 Content-Type: text/html 并在 SVG 强化逻辑之前退出,导致在没有提供密码的受密码保护分享中,浏览器进行内联渲染。
该漏洞并不仅限于受密码保护的流程。在我的测试中,非密码分享请求也返回了 text/html:
命令:
curl -svI "http://127.0.0.1:8080/api/file/share.php?token=437d7913884ace4b94fab8ce745a686a" 2>&1 | grep -iE "content-type"
观察到的结果: < Content-Type: text/html; charset=UTF-8
这证实即使在一般(非密码)情况下,响应也是 text/html,SVG 会内联渲染。
我捕获了对分享端点的原始抓取,其中包含在标头强化之前发出的 PHP 警告。快照(已删减):
命令:
curl -s "http://127.0.0.1:8080/api/file/share.php?token=437d7913884ace4b94fab8ce745a686a" | head -n 30
观察到的原始输出(已删减):
Deprecated: Constant FILTER_SANITIZE_STRING is deprecated in /data/data/com.termux/files/home/FileRise/src/controllers/FileController.php on line 1644
这些警告表明在标头强化之前已经产生了输出(弃用通知),使得后续的 header() 调用在这些运行中无法生效。
nosniff 的强化代码确实存在,但由于提前退出和输出,在许多代码路径中无法到达。在根因报告之后,维护者应用了解决控制流和输出顺序问题的更改。在 v2.7.1 中:
exit; 点已得到纠正/处理。最终验证(我在 v2.7.1 上的测试):
命令:
curl -svI "http://127.0.0.1:8080/api/file/share.php?token=fc911e48b0a30e9417a9020ef959784d" 2>&1 | grep -iE "content-type|content-disposition"
观察到的结果: < Content-Type: application/octet-stream < Content-Disposition: attachment; filename="xss-image.svg"; filename*=UTF-8''xss-image.svg
结果:浏览器被强制下载;SVG 没有内联渲染,XSS 负载未执行。我认为 v2.7.1 已在我的环境中解决了该问题。
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L — 评分:9.6(严重)
我在公告线程中记录了此依据,并请求使用 PR:N。
CNA 的 CVSS 向量反映了一个受限的威胁模型。
经验测试表明存在更严重且可复现的利用路径,这与标准评分规则下更高的 CVSS 3.1 基础评分一致。
鼓励独立评估者使用本文档中描述的观察到的利用条件来评估严重性,确保公开的严重性反映现实世界的影响,而不是一个范围狭窄的基线。
1649 | header('Content-Type: application/json; charset=utf-8'); 1651 | exit; 1657 | header('Content-Type: application/json; charset=utf-8'); 1659 | exit; 1664 | header('Content-Type: application/json; charset=utf-8'); 1666 | exit; 1670 | header("Content-Type: text/html; charset=utf-8"); 1693 | exit; 1699 | header('Content-Type: application/json; charset=utf-8'); 1701 | exit; 1719 | header('Content-Type: application/json; charset=utf-8'); 1721 | exit; 1725 | header('Content-Type: application/json; charset=utf-8'); 1727 | exit;
安全标头从约 1743 行开始: 1743 | header('X-Content-Type-Options: nosniff'); ... 1770 | header("Content-Disposition: attachment; ...");
~/FileRise $ curl -svI "http://127.0.0.1:8080/api/file/share.php?token=437d7913884ace4b94fab8ce745a686a" 2>&1 | grep -iE "content-type" < Content-Type: text/html; charset=UTF-8
Deprecated: Constant FILTER_SANITIZE_STRING is deprecated in /data/data/com.termux/files/home/FileRise/src/controllers/FileController.php on line 1644
...随后是 SVG 负载被输出并内联渲染。
~/FileRise $ curl -svI "http://127.0.0.1:8080/api/file/share.php?token=fc911e48b0a30e9417a9020ef959784d" 2>&1 | grep -iE "content-type|content-disposition" < Content-Type: application/octet-stream < Content-Disposition: attachment; filename="xss-image.svg"; filename*=UTF-8''xss-image.svg