Two RCE vulnerabilities in Notepad++ (CVE-2026-48778, CVE-2026-48800)

ringzeropirate1 pts0 comments

RCE in Notepad++ via XML Configuration Files: A Taint Analysis Journey — RingZero Pirate

CVE-2026-48778 | CVE-2026-48800 | CVSS 7.8 HIGH<br>Notepad++ v8.9.5 — Fixed in v8.9.6.1<br>Reading time: 10 minuti

Responsible Disclosure Note — All findings described in this article were communicated to Don Ho (maintainer of Notepad++) prior to publication. The upstream fix has been committed to the repository notepad-plus-plus/notepad-plus-plus. The complete timeline is provided at the bottom of the article.

Table of Contents<br>Introduction<br>Setup: Loading the Codebase<br>Threat Model and Attack Surface<br>Phase 1: Taint Analysis with Semgrep<br>Phase 2: Manual Verification of Findings<br>CVE-2026-48778: config.xml → ShellExecute<br>CVE-2026-48800: shortcuts.xml → ShellExecute<br>Proof of Concept<br>Fix Analysis and Residual Risk<br>Responsible Disclosure Timeline<br>Conclusions<br>1. Introduction<br>Notepad++ is one of the most widely used text editors on Windows, with over 28 million downloads. Its codebase is entirely open source and written in C++, making it an ideal target for static security analysis.<br>This writeup documents the discovery of two Remote Code Execution vulnerabilities found through taint analysis of the PowerEditor source code (v8.9.5). Both vulnerabilities share the same root cause: data read from XML configuration files flows directly into the Windows ShellExecute API without any validation, whitelist, or integrity check.<br>Affected files:<br>%APPDATA%\Notepad++\config.xml → CVE-2026-48778<br>%APPDATA%\Notepad++\shortcuts.xml → CVE-2026-48800<br>2. Setup: Loading the Codebase<br>The analysis started by extracting the PowerEditor.zip archive from the official Notepad++ repository and mapping the structure:<br>PowerEditor/<br>└── src/<br>├── Parameters.cpp (~8000 lines — config loading, XML parsing)<br>├── NppCommands.cpp (command dispatch — IDM_* handlers)<br>├── NppXml.h (pugixml wrapper)<br>├── WinControls/<br>│ ├── StaticDialog/RunDlg/RunDlg.cpp (Command::run → ShellExecute)<br>│ └── shortcut/shortcut.h (UserCommand class)<br>└── MISC/<br>├── PluginsManager/PluginsManager.cpp<br>└── Process/Processus.cpp<br>The XML parsing layer is handled by a thin wrapper around pugixml defined in NppXml.h:<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>// NppXml.h — the thin wrapper over pugixml<br>namespace NppXml<br>using Document = pugi::xml_document*;<br>using Element = pugi::xml_node;<br>using Node = pugi::xml_node;

[[nodiscard]] inline bool loadFileShortcut(Document doc, const wchar_t* filename) {<br>return doc->load_file(filename,<br>pugi::parse_cdata | pugi::parse_escapes |<br>pugi::parse_comments | pugi::parse_declaration);

// The key source function — reads a text node value<br>[[nodiscard]] inline const char* value(Node node) {<br>return node.value(); // raw pugi::xml_node::value()

Every call to NppXml::value() is a potential taint source : it returns raw string data from an XML file on disk, fully controlled by whoever last wrote that file.<br>3. Threat Model and Attack Surface<br>What can an attacker control?<br>Notepad++ stores its configuration in %APPDATA%\Notepad++\. This directory is writable by any process running as the same user — no elevated privileges required. An attacker can write to these files via:<br>VectorDescriptionSame-user processAny code running as the logged-in userArchive extractionA ZIP/RAR that extracts files into AppDataCloud sync poisoningOneDrive/Dropbox sync of a shared folder-settingsDir=PATHNPP command-line flag pointing to a custom config dirMalicious .lnk shortcutnotepad++.exe -settingsDir="C:\attacker\evil_config"The -settingsDir= vector is particularly stealthy: the attacker provides a self-contained directory with malicious XML files. The victim&rsquo;s real %APPDATA%\Notepad++ is never touched.<br>What is the sink?<br>The execution sink is ShellExecute, called in RunDlg.cpp:221:<br>// RunDlg.cpp — Command::run()<br>HINSTANCE res = ::ShellExecute(hWnd, L"open",<br>cmd2Exec, // ← executable path<br>args2Exec, // ← arguments<br>cwd2Exec, // ← working directory<br>SW_SHOW);

cmd2Exec is derived from _cmdLine, which is the Command object&rsquo;s internal state. The question is: what populates _cmdLine?<br>4. Phase 1: Taint Analysis with Semgrep<br>Since CodeQL CLI requires a compilable Windows project, I used Semgrep 1.163.0 with custom rules to trace data flows from NppXml::value() to ShellExecute.<br>Semgrep Rules<br>Seven rules were written covering direct flows, propagation through string2wstring, and memory sinks:<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>32<br>33<br>34<br>35<br>36<br>rules:<br>- id: npp-commandline-interpreter-xml<br>severity: ERROR<br>message: ><br>[EXEC_RCE][src:XML_CONFIG] _commandLineInterpreter read from config.xml<br>via NppXml::value, used as executable in Command::run() -> ShellExecute.<br>languages: [cpp]<br>pattern-either:<br>- pattern: |<br>const char* $CLI = NppXml::value($NODE);<br>...<br>_nppGUI._commandLineInterpreter = string2wstring($CLI);

- id: npp-xml-value-to-shellexecute<br>severity: ERROR<br>message: ><br>[EXEC_RCE][src:XML_CONFIG] NppXml::value() reaches ShellExecute.<br>languages: [cpp]<br>pattern-either:<br>- pattern: |<br>$VAR =...

notepad shellexecute value nppxml pugi analysis

Related Articles