一个 AI 编程助手明明被禁止修改敏感配置,却只因文件名末尾多了一个句点,就能绕过检查,最后仍写进原来的配置文件。
微软在 2026 年 10 月 13 日发布了 CVE-2026-103572 安全更新。微软给出的 CVSS 3.1 分数是 8.8,攻击向量为网络、需要用户交互;公开记录列出的受影响范围包括 Visual Studio 2022 17.14 的 17.14.0 至低于 17.14.42,以及 Visual Studio 2026 18.10 的 18.10.0 至低于 18.10.4。
漏洞研究者 Johann Rehberger 随后公开了技术分析。问题同时影响 Visual Studio Copilot 和 SQL Server Management Studio(SSMS)中的相关能力,根因位于共享的 Microsoft.VisualStudio.Copilot.Core.dll。真正值得所有 Agent 开发者记住的,不只是“Windows 文件名有怪规则”,而是更普遍的一条安全原则:授权检查看到的资源,必须和最终被操作的资源具有完全相同的身份。
两个字符串,为什么会指向同一个文件
假设 Agent 的文件工具不允许修改 .mcp.json。最直观的检查方式,是取出目标文件名后做一次字符串比较:
输入文件名 == ".mcp.json" → 拒绝
输入文件名 != ".mcp.json" → 继续2
.mcp.json. 与 .mcp.json 显然是两个不同的字符串,第一层检查因此放行。问题出在下一层:Windows 常用文件 API 会规范化文件名,保存时去掉末尾的 ASCII 句点。于是检查阶段看到的是 .mcp.json.,写入阶段真正落到磁盘上的却是 .mcp.json。
这就像门卫按手写姓名核对黑名单,仓库系统却会在入库前自动删掉姓名末尾的标点。门卫认为“张三。”不是“张三”,仓库最终登记的仍是“张三”。两套系统分别都按自己的规则工作,组合在一起却把禁止对象放了进去。
研究者披露的实现位于 EditsFunctionHandler.GetEditConfirmationTypeAsync。同一调查还发现了相对路径绕行:一个文本上以可信目录开头的路径,可以包含 ..,解析后实际离开允许目录。两类问题看起来不同,本质相同——策略判断发生在路径取得最终身份之前。
为什么一次文件覆盖会升级成代码执行
单独覆盖普通文本文件,影响通常有限;被覆盖的是“以后会决定运行什么”的配置文件,风险会完全不同。
公开分析给出了两条主要路径:
- Agent 绕过保护,修改 MCP 相关配置;
- 配置让 IDE 在后续流程中启动一个工具;
- 原本只是“写文件”的权限,跨过配置边界变成了“运行程序”。
在 Visual Studio 中,研究者还验证了另一条配置路径:覆盖搜索工具的位置后,IDE 在执行搜索时会启动被替换的程序。这里不需要把完整利用配置复制到自己的电脑上;重要的是看到权限的传递关系:
外部内容
↓ 间接提示注入
Agent 决定写文件
↓ 路径检查与操作语义不一致
敏感配置被修改
↓ IDE 信任并加载配置
命令执行2
3
4
5
6
7
因此,不能把根因简单归为“模型听了恶意提示”。间接提示注入只是让 Agent 尝试危险动作的入口;真正应当兜底的是工具层授权。即使模型已经被误导,文件工具也不应该让它改到被明确保护的配置。
用一个无害测试复现“身份错位”
下面的 Node.js 测试不会修改 Visual Studio、SSMS 或任何真实配置,也不执行命令。它只是把 Windows 的相关路径规则建模成纯函数,对比“直接比较字符串”和“先规范化再判断”两种策略。
import assert from 'node:assert/strict'
import path from 'node:path'
import test from 'node:test'
function normalizeWindowsComponent(component) {
if (component === '.' || component === '..') return component
return component.replace(/[ .]+$/u, '')
}
function canonicalizeForPolicy(input) {
const withWindowsSeparators = input.replaceAll('/', '\\')
const componentsNormalized = withWindowsSeparators
.split('\\')
.map(normalizeWindowsComponent)
.join('\\')
return path.win32.normalize(componentsNormalized).toLowerCase()
}
const protectedPaths = new Set([
canonicalizeForPolicy('C:\\repo\\.mcp.json'),
canonicalizeForPolicy('C:\\repo\\settings.VisualStudio.json')
])
function naivePolicy(input) {
return protectedPaths.has(path.win32.normalize(input).toLowerCase())
}
function canonicalPolicy(input) {
return protectedPaths.has(canonicalizeForPolicy(input))
}
test('直接比较会漏掉末尾句点别名', () => {
assert.equal(naivePolicy('C:\\repo\\.mcp.json.'), false)
})
test('规范化后会识别相同目标', () => {
assert.equal(canonicalPolicy('C:\\repo\\.mcp.json.'), true)
assert.equal(canonicalPolicy('C:\\repo\\temp\\..\\.mcp.json.'), true)
})2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
把代码保存为 path-guard.test.mjs 后运行:
node --test path-guard.test.mjs我使用 Node.js 24.19.0 实际运行了扩展后的 4 项测试:直接比较如预期漏检;规范化策略识别了末尾句点和 .. 形成的别名;普通源码文件没有被误判,结果为 4 项全部通过。
这是一项跨平台的策略回归测试,不是对 Windows 漏洞利用的复现。示例显式模拟了尾随句点规则,目的在于把“检查前后是否指向同一资源”变成可自动验证的不变量。
正确修复不是多加一个文件名黑名单
研究者检查补丁后发现,修复加入了 GetFullPath,在策略判断前去掉尾随句点并解析相对路径。这能处理公开披露的句点绕过,也能挡住一部分 .. 变体。
如果正在为 Agent 编写文件、补丁或工作区工具,可以把防线拆成四层:
- 按目标操作系统的语义取得规范绝对路径。 不要用 Linux 容器里的路径规则代替 Windows 客户端的真实规则;大小写、尾随空格和句点、设备名、替代路径语法都可能改变身份。
- 先解析,再授权。 目录允许列表与敏感文件列表都应比较规范结果,而不是未经处理的用户输入。
- 检查与操作使用同一个结果。 通过校验后,不要再回到原始字符串执行写入,否则会重新引入“检查一个路径、写另一个路径”的差异。
- 把文件系统重定向算进威胁模型。 符号链接、junction、reparse point 和检查后替换会带来新的竞态;高权限工具应尽量基于已打开句柄核对最终对象,并限制 Agent 进程本身的权限。
单纯把 .mcp.json. 加入黑名单没有意义:攻击者还可以增加多个句点、尾随空格、相对路径片段,或者寻找下一个具有相同语义差异的名字。安全规则应围绕“最终对象”设计,而不是围绕某个见过的字符串设计。
现在应该做什么
Visual Studio 用户可以先确认版本并升级:
- Visual Studio 2022 17.14:升级到 17.14.42 或更高版本;
- Visual Studio 2026 18.10:升级到 18.10.4 或更高版本。
这两个修复版本都在 10 月 13 日发布,微软的发布说明均列出了 CVE-2026-103572。SSMS 用户也应通过官方安装器更新到当前受支持的最新版本;公开 CVE 记录没有给出一个可独立核对的 SSMS 最低修复版本,因此不应根据 Visual Studio 的版本号替它推算。
如果曾让 Copilot 或 SSMS Agent 处理不可信仓库、网页内容、数据库文本或第三方 MCP 返回值,还应检查:
- MCP 配置中是否出现陌生服务器、命令或被自动启用的条目;
- Visual Studio 设置中的外部工具路径是否发生异常变化;
- IDE 或其子进程是否启动过无法解释的程序;
- 相关开发凭证、数据库凭证和云端令牌是否需要轮换。
这些检查不能替代升级,也不表示所有旧版本机器已经被利用。CISA 在公开记录中将当时的已知利用状态标为 none;它说明尚无已知在野利用证据,不代表漏洞无法利用,因为研究者已经公开演示了完整链条。
这次漏洞给 Agent 工具设计留下的教训很具体:模型可以不可信,输入可以带提示注入,授权层仍必须可靠。真正的安全边界不是一句“不要修改配置”的系统提示,而是工具在面对任何等价路径时,都能认出那仍是同一个受保护对象。
💬 评论