
Notepad++ v8.9.6.2 — Attacker-Controlled UserCommand Execution via HMAC Race Condition
| GHSA | GHSA-qm4c-qg8p-qfcr |
| CVE | CVE-2026-52885 |
| Severity | High |
| Affected | v8.9.6.2 |
| Patched | v8.9.6.4 |
| Patch Commit | 4f7563c |
| Affected File | PowerEditor/src/NppCommands.cpp :4328–4359 |
| Platform | Windows 11 Pro x64 |
| Privilege Required | Standard user (write access to shortcuts.xml) |
| Researcher | v3s9er |
| Disclosed | 2026-06-05 |
Notepad++ v8.9.6.2 introduced an HMAC-SHA256 integrity check on shortcuts.xml to prevent injection of arbitrary UserCommand entries.
The check has a TOCTOU flaw: the HMAC is verified against the on-disk file at trigger time, but the command that executes is read from the in-memory _userCommands vector loaded at startup. These two sources are never re-synchronized.
An attacker who can write to shortcuts.xml can plant a malicious command before launch, let Notepad++ load it into memory, restore the legitimate file on disk, then trigger the command. The HMAC check passes (disk is clean), but the malicious payload executes (memory is stale).
The integrity mechanism validates the current on-disk shortcuts.xml, while command execution uses a previously loaded in-memory representation. Because the two data sources are independent, an attacker can alter the file before startup, allow it to be parsed into memory, then restore the original file before command execution — causing the HMAC check to pass while the malicious payload runs.
NppCommands.cpp lines 4328–4359:
// TIME OF CHECK — reads on-disk file
std::string currentHMAC = computeHMAC(
getMachineGUID(),
getFileContent(nppParams.getShortcutsPath().c_str())
);
if (currentHMAC != nppGUI._shortcutsXmlHmacInConfig) { return; }
// validated against DISK
// TIME OF USE — reads in-memory vector (loaded at startup, never refreshed)
int i = id - ID_USER_CMD;
const vector<UserCommand>& cmds = nppParams.getUserCommandList();
UserCommand ucmd = cmds[i];
Command cmd(string2wstring(ucmd.getCmd(), CP_UTF8));
cmd.run(_pPublicInterface->getHSelf()); // executes payload from MEMORY
| Operation | Source | Timing |
|---|---|---|
| CHECK (HMAC) | Disk file | At trigger time |
| USE (exec) | Memory vector | Loaded at startup |
| GAP | Attacker-controlled | Startup → Trigger |
Pre-condition: write access to shortcuts.xml
notepad++.exe%AppData%\Notepad++\Steps:
shortcuts.xml on disk with a malicious version (cmd = calc.exe)_userCommands is populated from the malicious fileshortcuts.xml to disk — HMAC now matches config.xmlWM_COMMAND(21000) (ID_USER_CMD+0) or press Ctrl+Alt+P_userCommands[0] from memory → calc.exe runsResult: execution of attacker-controlled commands under the privileges of the current user. No admin, no UAC, no user warning.
See poc_8962_toctou.ps1.
Requirements:
doLocalConf.xml present in the Notepad++ directory (portable mode)config.xml with a valid HMAC for the legitimate shortcuts.xml.\poc_8962_toctou.ps1 -NppPath "C:\path\to\notepad++.exe"
Verified: calc.exe spawned (PID 35932), disk shows legitimate shortcuts.xml throughout.
The fix (commit 4f7563c) moves HMAC validation to startup, alongside the initial file parse. Check and Use now operate on the same object at the same time, eliminating the TOCTOU window.
| v8.9.6.2 (vulnerable) | v8.9.6.4 (patched) | |
|---|---|---|
| HMAC check | At trigger time, re-reads disk | At startup, same bytes as parse |
| Command source | Memory loaded at startup | Memory validated at startup |
| TOCTOU window | Startup → Trigger | Eliminated |
| Time | Event | State |
|---|
| T+0.0s | Attacker writes MALICIOUS shortcuts.xml | disk=MALICIOUS |
| T+0.0s | Attacker launches Notepad++ | disk=MALICIOUS |
| T+0.1s | Notepad++ reads shortcuts.xml → _userCommands[0]="calc.exe" | mem=MALICIOUS |
| T+4.2s | Notepad++ fully loaded | mem=MALICIOUS |
| T+4.2s | Attacker restores LEGIT shortcuts.xml | disk=LEGIT / mem=MAL |
| T+4.7s | Attacker sends WM_COMMAND(21000) | |
| T+4.7s | HMAC check: reads LEGIT file from disk → PASSES | CHECK OK |
| T+4.7s | Exec: reads _userCommands[0] from MEMORY → calc.exe | EXPLOIT |
| T+4.7s | calc.exe spawned (PID 35932) | CONFIRMED |
| Type | Arbitrary Command Execution |
| Admin Required | No |
| UAC Prompt | No |
| Detectability | Low — disk shows legitimate file throughout; payload is memory-only |
| CVSS (est.) | AV:L / AC:H / PR:L / UI:N / S:U / C:H / I:H / A:H |