导语:Akaunting 存储型 XSS 漏洞(CVE-2026-118855,CVSS 7.0)再次提醒我们:在 Laravel 生态中,一个被遗忘的
{{ }}与{!! !!}之差,就足以让财务系统的发票编辑页沦为攻击者的脚本执行沙箱。本文以一线架构师视角,完整还原从发票导入到隐藏域渲染的攻击链,并给出可落地的纵深防御方案。
一、Akaunting 存储型 XSS 漏洞全景速览
Akaunting 是一款基于 Laravel 构建的开源在线会计与发票管理平台,被大量中小企业用于财务记账、发票开具与客户对账。2026 年 11 月,GitHub Advisory Database 披露了编号为 CVE-2026-118855 的高危漏洞,CVSS v4 基础评分为 7.0,属于典型的存储型 XSS(Stored Cross-Site Scripting)。
与反射型 XSS 不同,存储型 XSS 的恶意载荷会被持久化到数据库中,任何后续访问该数据的用户都会成为受害者。在财务系统中,这意味着攻击者可以借助一张“看起来正常”的发票,在管理员或财务主管打开编辑表单的瞬间窃取会话令牌、篡改账目甚至横向移动。
| 评估维度 | 详情 | 风险等级 |
|---|---|---|
| CVE 编号 | CVE-2026-118855 | 已公开 |
| CVSS v4 评分 | 7.0(High) | 高危 |
| 攻击向量 | Network / 低复杂度 | 远程可利用 |
| 所需权限 | 已认证 + 发票创建权限 | 内部威胁面 |
| 用户交互 | 受害者打开发票编辑表单 | 被动触发 |
| 影响范围 | 机密性高 / 完整性高 / 可用性无 | 数据泄露风险 |
该漏洞的“前置权限”看似较高(需要发票创建权限),但在实际企业环境中,销售、出纳、外包记账人员往往都具备该权限。一旦其中任一账号被钓鱼或内部人员恶意利用,即可对财务主管发起定向攻击,形成典型的权限提升链。
企业级架构规范
flowchart TD
subgraph 攻击者层 ["攻击者与入口层 (Attacker & Entry Tier)"]
A["已认证用户 (发票创建权限)"] --> B["发票导入接口 (Import Request)"]
end
subgraph 校验层 ["输入校验与持久化层 (Validation & Persistence Tier)"]
B --> C["Document 请求校验 (缺失 status 白名单)"]
C --> D[("数据库持久化 (恶意 HTML 落库)")]
end
subgraph 渲染层 ["Blade 渲染与输出层 (Render Tier)"]
D --> E["编辑表单加载 (Edit Form)"]
E --> F["hidden.blade.php 原始输出 {!! !!}"]
end
subgraph 受害层 ["受害者与执行层 (Victim Tier)"]
F --> G["财务主管浏览器 (同源脚本执行)"]
G --> H["会话令牌窃取 / 账目篡改"]
end
subgraph 防御层 ["纵深防御层 (Defense Tier)"]
I["输入白名单校验"] -.-> C
J["Blade 自动转义 {{ }}"] -.-> F
K["CSP 内容安全策略"] -.-> G
end
二、漏洞根因拆解:三条链路同时失守
2.1 入口链路:发票导入缺少 status 字段白名单
根据官方公告,攻击者通过发票导入功能将 HTML 注入到发票的 status 字段。在 Akaunting 3.3.0 的 app/Http/Requests/Document/Document.php 中,发票创建/导入请求对多数字段做了类型与长度校验,但 status 字段的校验规则并未限定为枚举值(如 draft、sent、paid),也未做 HTML 字符过滤。
这意味着攻击者可以构造如下导入数据,将任意 HTML 片段写入数据库:
{
"invoice_number": "INV-2026-0001",
"customer_id": 42,
"status": "
",
"items": [
{ "name": "Consulting", "quantity": 1, "price": 1000 }
]
}
在 Laravel 中,status 这类有限状态字段应当使用 Rule::in([...]) 或枚举类进行强约束,而不是依赖前端下拉框。服务端校验才是唯一可信边界,前端限制永远只是体验优化。
2.2 渲染链路:hidden.blade.php 使用原始输出
漏洞的第二个关键点在 resources/views/components/form/input/hidden.blade.php。该组件在渲染隐藏输入框时,使用了 Blade 的原始输出语法 {!! $value !!},而非自动转义的 {{ $value }}。当编辑表单加载时,数据库中存储的 status 值被原样拼接到 <input type="hidden" value="..."> 中,攻击者的 onerror 载荷随即在受害者浏览器中执行。
<input
type="hidden"
name="{{ $name }}"
value="{!! $value !!}"
{{ $attributes }}
/>
正确的写法应当始终使用自动转义语法:
<input
type="hidden"
name="{{ $name }}"
value="{{ $value }}"
{{ $attributes }}
/>
2.3 触发链路:编辑表单成为“被动触发点”
存储型 XSS 的可怕之处在于触发与投递分离。攻击者完成导入后即可退出,而受害者(通常是拥有更高权限的财务主管或管理员)在正常业务操作中打开该发票的编辑表单,脚本便在同源上下文中执行。由于 Akaunting 的会话 Cookie 默认未设置 HttpOnly 之外的额外隔离,攻击者可读取页面 DOM、发起同源 API 请求,甚至篡改其他发票数据。
财务系统的 XSS 危害远高于普通站点:攻击者可利用已认证会话直接调用 Akaunting 内部 API 修改收款账户、导出客户信息或伪造付款指令。请勿将此类漏洞简单归类为“弹窗恶作剧”。
三、Akaunting 存储型 XSS 的复现与验证路径
在授权测试环境中,可按以下步骤验证漏洞是否存在。请注意:任何未授权的测试均属违法行为,以下流程仅用于自有部署的安全自检。
- 步骤一:使用具备发票创建权限的账号登录 Akaunting 3.3.0。
- 步骤二:通过发票导入功能上传包含恶意
status字段的 CSV/JSON 文件。 - 步骤三:确认发票已创建,并在数据库中观察到原始 HTML 被持久化。
- 步骤四:使用另一具备发票编辑权限的账号打开该发票编辑页,观察脚本是否执行。
- 步骤五:检查浏览器控制台与网络请求,确认同源 API 是否可被脚本调用。
点击展开:本地 Docker 环境快速搭建与验证命令
# 拉取 Akaunting 3.3.0 镜像并启动 docker run -d --name akaunting-test -p 8080:80 -e APP_URL=http://localhost:8080 akaunting/akaunting:3.3.0 # 初始化完成后访问 http://localhost:8080 完成安装 # 使用具备发票权限的账号执行导入验证 # 验证完成后务必销毁容器: docker rm -f akaunting-test
四、修复方案与 Laravel 纵深防御工程实践
4.1 立即修复:输入白名单 + 输出转义双管齐下
针对 Akaunting 存储型 XSS,官方修复方向集中在两处:一是为 status 字段增加枚举校验,二是将隐藏域组件改为自动转义输出。对于无法立即升级的自建部署,可先通过以下补丁临时缓解:
public function rules()
{
return [
'status' => [
'required',
'string',
Rule::in(['draft', 'sent', 'paid', 'partial', 'overdue']),
],
// 其他字段规则保持不变
];
}
4.2 纵深防御:CSP 与 Blade 规范治理
单点修复只能解决已知漏洞,架构层面需要建立输出编码默认安全的工程规范。建议在 Laravel 项目中推行以下治理措施:
- Blade 语法审计:通过静态扫描工具(如 PHPStan 自定义规则或 grep 规则)禁止业务视图中出现
{!! !!},确需原始输出时必须附带安全评审注释。 - CSP 内容安全策略:在 Nginx 或 Laravel 中间件中下发
Content-Security-Policy,禁止内联脚本与未授权域加载,将 XSS 危害降到最低。 - Cookie 加固:为会话 Cookie 设置
HttpOnly、Secure、SameSite=Lax,降低令牌被脚本读取的风险。 - 导入通道隔离:对 CSV/JSON 导入数据统一走 DTO 映射与类型转换,禁止将原始字符串直接写入模型。
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self';" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always;
对于财务、ERP 这类高价值系统,建议将 CSP 从 Report-Only 模式起步,收集两周违规报告后再切换为强制模式,避免一次性上线导致业务脚本被误杀。
五、方案对比:传统修补 vs 体系化防御
| 评估维度 | 传统修补方案 | 体系化防御方案 | 架构收益 |
|---|---|---|---|
| 输入校验 | 仅前端下拉框限制 | 服务端枚举 + DTO 白名单 | 阻断注入源头 |
| 输出编码 | 依赖开发者记忆 | Blade 自动转义 + 静态扫描 | 默认安全 |
| 运行时防护 | 无 | CSP + HttpOnly + SameSite | 降低利用面 |
| 漏洞发现 | 被动等待通告 | SAST/DAST 常态化扫描 | 左移治理 |
| 应急响应 | 临时打补丁 | 版本升级 + 回归测试 | 可持续演进 |
六、总结与行动清单
Akaunting 存储型 XSS 漏洞(CVE-2026-118855)本质上是一个输入校验缺失 + 输出转义误用的经典组合缺陷。它提醒所有 Laravel 架构师:框架提供的自动转义是“默认安全”,但一旦开发者主动选择 {!! !!},安全责任就完全转移到了代码审查环节。
建议运维与研发团队立即执行以下行动清单:
- 核查当前 Akaunting 版本,关注官方 3.3.x 后续补丁发布并及时升级。
- 全局搜索项目中的
{!! !!}原始输出,逐条评估并替换为{{ }}。 - 为所有有限状态字段补充服务端枚举校验,尤其是导入通道。
- 在反向代理层下发 CSP 与安全响应头,作为最后一道防线。
- 将会话 Cookie 设置为
HttpOnly+Secure+SameSite。
本文核心技术背景源自海外一线安全研究通告:
GitHub Advisory Database — GHSA-9xwp-mf2p-p42r (CVE-2026-118855)
NVD — CVE-2026-118855 详情页
Fluid Attacks — Sabbath 安全通告
