Notepad++ 8.9.9 修了 6 个漏洞,一个 CVE 号都没给:其中第 2 条是第 1 条的修复被绕过

先说结果:Notepad++ 8.9.9 在 2026 年 11 月 6 日发布,一次修了 6 个漏洞,官方页面上一个 CVE 号都没给——六个条目后面全部写着 “CVE ID not assigned yet”。

中间我栽了两个跟头。第一个是看到”没 CVE 号”就差点把这事归到”例行维护”里;第二个是读完之后本能地想写”又一个编辑器 RCE”,结果六个读完发现没有一个能网络打进来。

这篇是逐条读公告的过程记录,最后有一节讲这六个漏洞其实是同一族的。

配图

一、先把六个摊开

官方发布页(/news/v899-released/)上那行字有个小插曲:写成了 “6 vulnérabilités are fixed”——法语词混进了英文句子。这条我留着,因为它说明这页是手写的,不是自动生成的 changelog。

六条原文对应的 GHSA 编号,我一个个点进去读完了:

#GHSA一句话谁能触发后果
1pg39-63h5-cx35WM_COPYDATA 发送方认证缺失(UIPI 绕过)本地低完整性进程强制打开任意文件
25jjv-2h6g-63p5借 HWND 绕过 #1 的修复,经 NppExec 执行命令本地中完整性进程条件性高完整性代码执行
3h5cv-65m7-x97v剪贴板二进制长度不校验任意本地进程堆内存泄露(写进你的文档)
4w7wv-vg94-7pvpUAC 保存代理 TOCTOU同用户本地进程替换写进受保护文件的字节
5j327-595g-jg4cNPPM_MENUCOMMAND 越界索引任意本地进程越界读命令串并 ShellExecute
6554x-h7h6-gqfmMIME Tools 的 SAML 解码堆溢出打开一份恶意文件崩溃 / 潜在任意代码执行
配图

注意第 5 和第 6 的来源不一样:第 6 条的公告挂在 npp-plugins/mimetools 仓库下,不是主仓库。也就是说这 6 条里有 1 条是插件侧的账,只是随版本一起发了。

二、第 1 和第 2 条其实是一件事

这是我读到第二个跟头的地方。

第 1 条(pg39):Notepad++ 为了让资源管理器的”用 Notepad++ 编辑”在提权状态下也能用,启动时对 WM_COPYDATA 调了 ChangeWindowMessageFilterEx(..., MSGFLT_ALLOW, NULL)——等于把这个消息从 UIPI(用户界面特权隔离)的拦截名单里豁免了。但消息处理端从来不检查发送者是谁。

洞就在这儿:消息的 wParam 里明明带着发送方窗口句柄,代码没读。窗口类名 "Notepad++" 又是固定的,FindWindow 一抓就有。所以一个低完整性进程(浏览器渲染进程、保护模式进程那种)只要发一条 dwData=2 的 WM_COPYDATA,就能让中完整性的 Notepad++ 打开任意文件并切成当前标签页。

公告里那张对照表是我见过最干净的验证方式:同一个发送者、同一条消息、同样的完整性差距,只喂给两个程序——

NPP(allow-listed)                 SendMessage ret=1 GetLastError=0
CONTROL(notepad,not-allow-listed) SendMessage ret=0 GetLastError=5

notepad.exe 直接 ACCESS_DENIED。差别只有那一行 MSGFLT_ALLOW。

配图

第 2 条(5jjv)是给第 1 条打补丁时留下的窟窿。8.9.8 加了 isSenderTrusted(wParam),逻辑是:把 wParam 当作发送方窗口 → 查它的所属进程 → 要求完整性 ≥ Medium,且镜像路径等于接收方的 notepad++.exe 路径。

问题在于:wParam 是调用者自己填的,Windows 不会把它和真正调用 SendMessageW 的进程绑起来。

所以攻击者只要开一个正常的 Notepad++ 当”人质”(-multiInst,同一个 exe 路径,中完整性),把这个诱饵窗口的 HWND 填进 wParam,数据用自己进程的——接收方就会去验证那个无辜的诱饵,然后收下攻击者的数据。

公告作者把前置条件列得很清楚,一共五条,缺一不可:

  • 装了可选插件 NppExec(0.8.12.1)
  • NppExec 在目标进程里已经初始化过
  • 目标 Notepad++ 已经是提权状态
  • 攻击者已在同一交互会话拿到中完整性代码执行
  • 能起一个同路径的中完整性诱饵实例

满足之后,发一条 COPYDATA_PARAMS,里面塞 4656 字节的 CmdLineParamsDTO,_pluginMessage 填 NppExecScript=<脚本>,NppExec 就会在高完整性上下文里跑这个脚本。验证数据:

  • 提权目标 PID 33212,High IL(0x3000)
  • 诱饵 PID 2252,Medium IL(0x2000)
  • 标记文件里写的是 NPP_WMCOPYDATA_RCE_target_pid=33212
配图

它明确不是网络 RCE。 不需要 UAC 弹窗、不创建新提权、不复用远程通道——它复用的是”已经提权的那个进程的令牌”。作者自己给的建议评分是 CVSS 3.1 AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H = 7.0,AC:H 就是给那五条前置条件的。

顺带说个我读到才注意的细节:作者在公告里写,他打不开第 1 条的原始公告,所以无法判断自己这条算不算私有重复(private duplicate)。这种”公开公告之间互相看不见”的情况,本身就是漏洞披露流程里的问题。

三、第 5 条:同一条路,换了个消息

第 5 条的攻击面跟第 1、2 条是同一族——一个固定的窗口类名,加一条任何人都能发的窗口消息,加一个没做的校验。

NPPM_MENUCOMMAND 的值是 WM_USER + 1000 + 48 = 2072。处理它的代码只有两行:把 lParam 当命令 ID 传进去,return TRUE。没有发送方校验。

进去之后才是真正的问题:

if ((id >= ID_MACRO) && (id < ID_MACRO_LIMIT)) {
    int i = id - ID_MACRO;
    vector<MacroShortcut>& theMacros = nppParams.getMacroList();
    macroPlayback(theMacros[i].getMacro());        // 没有 size() 检查
}
else if ((id >= ID_USER_CMD) && (id < ID_USER_CMD_LIMIT)) {
    int i = id - ID_USER_CMD;
    const vector<UserCommand>& theUserCommands = nppParams.getUserCommandList();
    UserCommand ucmd = theUserCommands[i];          // 没有 size() 检查
    Command cmd(string2wstring(ucmd.getCmd(), CP_UTF8));
    cmd.run(...);                                   // 走到 ShellExecute
}

ID_MACRO 是 20000、上限 20499;ID_USER_CMD 是 21000、上限 21499。每段 500 个槽。而默认安装里这两个 vector 都是空的——因为生成 shortcuts.xml 的模板里只有被注释掉的示例。

于是 i 可以取 0 到 498,从一个空 vector 上越界读一个 UserCommand 出来,把它的命令串转成宽字符,丢给 ShellExecute。全程不需要用户点任何东西。

这里有个特别值得说的点:8.9.8 确实加了 shortcuts.xml 的 HMAC 完整性校验。但那个校验只证明”这个文件的内容没被改过”,它不校验”你拿哪个索引去取列表里的元素”。所以正常安装下这道门是开着的,越界索引直接过。

公告里还老实写了一句:只有 ID_USER_CMD 那一支被观察到真的走到了 ShellExecute,ID_MACRO 那一支在测试里没有可观察效果,因为 getMacro() 不一定解引用那个越界对象。这种”我只证明了这一半”的写法,比含糊其辞有用得多。

四、第 3 条:两个剪贴板格式,长度对不上

这条是我觉得最”日常”的一个——它不是提权,是把内存里的东西写进你正在编辑的文档。

Notepad++ 的二进制复制粘贴用了两个剪贴板格式:CF_TEXT 放字节,一个私有注册格式(名字就叫 Notepad++ Binary Length)放长度。粘贴的时候,长度直接拿私有格式的值当 CF_TEXT 缓冲区的大小用——不比对两者是否一致,也不调 GlobalSize。

于是任何本地进程都可以自己发布这两个格式,声明一个远大于实际缓冲区的长度。实测:三个字节的 CF_TEXT,声明长度 4096,保存出来的文档正好长了 4096 字节,其中后 4093 字节是进程堆里的活数据。

一个细节让我停了一下:整个仓库里 GlobalSize 只被调用了一次,在一条无关的编码转换路径上。也就是说这个函数在这个项目里基本是”知道有,但没用在该用的地方”。

公告还标了一处没复现的路径:剪贴板历史面板里也有同样的不校验长度,而那个面板是被 WM_DRAWCLIPBOARD 自动驱动的——面板开着的时候,连用户动作都不需要。作者明确写了”没有在测试环境里复现,仅记录给维护者”,这个分寸感是对的。

五、第 4 条:UAC 提权保存的 TOCTOU

场景很常见:你拿 Notepad++ 改一个需要管理员权限的文件,保存,弹 UAC,你点”是”。

流程是这样的:中完整性的编辑器把内容写到 %TEMP%\npp-<毫秒>.tmp,关掉文件句柄,然后用 runas 拉起同一个 exe,把临时文件路径和目标路径当命令行参数传过去。提权后的辅助进程重新按路径打开源文件,CopyFileW(source, destination, FALSE)。

问题:从”关句柄”到”重新打开”之间,那个临时文件是可替换的。同用户的本地进程盯着 %TEMP%,等句柄一关就抢独占,把内容换掉。用户点的那次 UAC 是真的,被换掉的是数据。

公告的验证数据很硬:

  • 对照组最终内容 SHA-256 A923384BDE…54DC20
  • 攻击前源文件 3FDE7AC799…1BE482
  • 替换后 1207BE52DF…69AF4A03
  • UAC 通过后,受保护目标文件内容 = 替换后的 SHA-256,一致

评分 CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:N/I:H/A:H = 6.6。C:N 是诚实的——它没证明能读,只证明能写。

这条的修复建议我抄下来了,因为它适用于所有”跨提权边界搬数据”的设计:不要把操作拆成两个可变路径字符串去过边界。 要么保持原句柄、用句柄复制绑定文件对象,要么直接把字节流过认证 IPC。提权端应该只打开一次源和目标,拒绝重解析点,在同一个句柄上校验身份和内容——绝不”先验证一个路径实例,再从后来的重开里拷贝”。

六、第 6 条:插件那边的三个溢出点

MIME Tools 是默认安装就带的插件(这条很重要,它意味着不是”你装了才有”)。SAML Decode 的流程是:URL 解码 → Base64 解码 → 可选 DEFLATE 解压。输出缓冲区按 SAML_MESSAGE_MAX_SIZE = 200,000 字节分配。

三个地方能越界:

  1. 解压炸弹:tinf_uncompress() 调用时没限制输出缓冲区的容量。压缩包解到 20 万字节以上,literal 输出、match 复制、非压缩块复制全都会写到堆边界外面
  2. 明文 XML 的 memcpy:Base64 解出来如果是明文(以 <?xml 或 <saml 开头),直接 memcpy 进 dest,不校验 base64DecodedLen 有没有超过 200,000
  3. 第二次 memcpy:解压成功后 memcpy(dest, inflatedText, inflatedTextLen),而 inflatedTextLen 前面就没被限住,等于第二次溢出

复现步骤短到有点荒谬:打开 Notepad++(默认安装)→ 打开那份构造好的 .b64.txt → Ctrl+A → 插件 → MIME Tools → SAML Decode。约 30 万字节往 20 万字节的堆缓冲区里灌,直接崩。

分类是 CWE-122(堆溢出)+ CWE-409(解压炸弹)。

七、六个读完,共同点只剩一句

我最初以为这是”六个独立的漏洞凑在一个版本里发”。读完之后发现不是。

配图

六个里有四个(1、2、3、5)是同一族:本地低权限进程,去打同用户的高权限窗口。 它们的共同前提是——Notepad++ 用窗口消息做单实例 IPC,而且用消息里调用者可控的字段当认证依据。

第 2 条作者的修复建议写得比我说得清楚:不要把 WM_COPYDATA.wParam 当认证原语。 安全敏感的单实例 IPC 应该走能拿到操作系统认证对端身份的信道——带合适 DACL 和强制标签(mandatory label)的命名管道,加客户端 PID 与令牌校验。如果确实要支持跨完整性打开文件,就只暴露一个窄化的”打开文件”操作,并且从低完整性调用方那里拒绝 _pluginMessage、会话/工作区加载这一类有副作用的命令行选项。

这句话对做 Windows 桌面安全的同学基本可以当模板抄。

另外要注意的是,这一族里没有一个是网络可达的。所以标题里那句”可触发插件命令执行”如果被读成”远程命令执行”,就偏了。第 2 条的 7.0 分里,AV:L 和 AC:H 两个标记就是给这件事的。

从沙箱视角看更有意思:低完整性进程就是浏览器渲染进程的处境。一个渲染进程被打穿之后,本来应该还有一层完整性边界——而 Notepad++ 的这个豁免,是把那层边界在自己的窗口上拆掉了。 它不是沙箱逃逸的完整链,但它是链上很好用的一环。

八、给你的三条判断

① 不是紧急 RCE,但 8.9.9 该升。 六个里没有一个不需要本地代码执行就能打。所以它不像”浏览器 0day”那样要今晚就推。但第 1、2、5 三条的前提是”攻击者已经在你机器上拿到了中/低完整性执行”——那个前提在一台被钓鱼过的开发机上一点都不奢侈。

② 检查一下你的 Notepad++ 是不是以管理员身份跑的。 这是第 1、2 条的放大器。以管理员身份跑编辑器(很多人为了方便改 hosts、改配置就这么用)会把”强制打开一个文件”升级成”以高完整性打开一个文件”,把第 2 条从”不成立”变成”成立”。

③ NppExec 装了的话,优先级往上提一档。 第 2 条的代码执行面完全靠它落地。不装 NppExec、或者装了但从不初始化,这条基本打不动。反过来,装了又提权跑,它就是那个把”打开文件”变成”执行命令”的放大器。

另外顺手检查两个配置项:剪贴板历史面板如果开着,第 3 条那处未复现的自动路径就多一个触发面;shortcuts.xml 的 HMAC 校验保护的是文件内容,不保护索引,别把它当成第 5 条的缓解措施。

九、最后

这篇最麻烦的地方不是读代码,是没有 CVE 号。六个条目全靠 GHSA 编号串起来,时间线要从 commit 标题里拼:de0a77ba… 标题是 “Fix Notepad++ UIPI Bypass via WM_COPYDATA”,引用了第 1 条;紧接着 67e755f0… 又加固了同一个检查;而第 2 条的借 HWND 手法绕过的正是这两个 commit 引入的检查。

也就是说,8.9.8 → 8.9.9 这中间,是一次”修复 → 被绕过 → 再修复”的完整回合,而官方发布页对这件事的全部表述是两句并列的 “Fix WM_COPYDATA …”。

我不觉得这是隐瞒——作者在公告里主动写了”这应该先按第 1 条的不完整修复来分类”,还主动说明自己打不开原始公告。真正的问题是编号:没有 CVE,资产管理系统就抓不到,扫描器也不会告诉你有这一条。你只能靠人读版本公告。

所以如果你在做软件资产管理:这一版需要手工登记,别指望漏洞库。

参考来源(全部为官方公告原文,CVE 编号尚未分配):

  • Notepad++ v8.9.9 发布公告:https://notepad-plus-plus.org/news/v899-released/
  • 下载页与完整更新列表:https://notepad-plus-plus.org/downloads/v8.9.9/
  • GHSA-pg39-63h5-cx35(UIPI 绕过,WM_COPYDATA 发送方不校验):https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-pg39-63h5-cx35
  • GHSA-5jjv-2h6g-63p5(借 HWND 绕过,经 NppExec 条件性高完整性执行):https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-5jjv-2h6g-63p5
  • GHSA-h5cv-65m7-x97v(剪贴板长度不校验,堆泄露):https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-h5cv-65m7-x97v
  • GHSA-w7wv-vg94-7pvp(UAC 保存代理 TOCTOU):https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-w7wv-vg94-7pvp
  • GHSA-j327-595g-jg4c(NPPM_MENUCOMMAND 越界索引):https://github.com/notepad-plus-plus/notepad-plus-plus/security/advisories/GHSA-j327-595g-jg4c
  • GHSA-554x-h7h6-gqfm(MIME Tools SAML 解码堆溢出):https://github.com/npp-plugins/mimetools/security/advisories/GHSA-554x-h7h6-gqfm
  • 回归问题反馈帖:https://community.notepad-plus-plus.org/topic/27707/notepad-release-8.9.9

暂无介绍....

延伸阅读:

Notepad++ 8.9.9 修了 6 个漏洞,一个 CVE 号都没给:其中第 2 条是第 1 条的修复被绕过

Notepad++ 8.9.9 于 2026-11-06 发布,一次修 6 个漏洞,官方页面六个条目全部标注 CVE 未...

itadol5j
2026年11月11日
企业 AI 安全能力建设路线图:从同步规划到持续运营的五个阶段

把 NIST AI RMF 的风险管理语言翻译成工程动作:五个阶段,每个阶段带交付物、过闸标准和失败模式。含 OWASP...

itadol5j
2026年11月10日
Claude Fable 5.2 拆解:14 项跑分 13 项超上代,但它自己砍了三刀

Anthropic 11 月 9 日发 Fable 5.2 与 Mythos 5.2:同一底层模型、两套拦截器。14 项...

itadol5j
2026年11月10日
不是桃色新闻:黄仁勋这 8 个月,和一份没人执行的备忘录

把时间线摆出来才是新闻:鲁比奥备忘录春天就在高层传阅、定性为情报资产,五月黄仁勋随特朗普访华、九月坐白宫国宴主桌,而当事...

itadol5j
2026年11月9日
AiSOC:6741 星的开源 AI SOC,先学会了对自己说实话

AiSOC 现 6741 星。它最特别的地方不是 AI 分诊,是那份 REPOSITORY_REALITY 自查报告——...

itadol5j
2026年11月9日