Critical severity9.6GHSA Advisory· Published May 8, 2026· Updated May 13, 2026
CVE-2026-43944
CVE-2026-43944
Description
electerm is an open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client. From versions 3.0.6 to before 3.8.15, electerm is vulnerable to arbitrary local code execution via deep links, CLI --opts, or crafted shortcuts. Exploit requires clicking a crafted electerm://... link or opening a crafted shortcut/command that launches electerm with attacker-controlled opts. This issue has been patched in version 3.8.15.
Affected products
1Patches
30599e67069b0Security: do not allow unsafe props when open deep link
6 files changed · +39 −19
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "electerm", - "version": "3.9.0", + "version": "3.9.5", "description": "Terminal/ssh/telnet/serialport/sftp client(linux, mac, win)", "main": "app.js", "bin": "npm/electerm",
package-lock.json+2 −2 modified@@ -1,12 +1,12 @@ { "name": "electerm", - "version": "3.9.0", + "version": "3.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electerm", - "version": "3.9.0", + "version": "3.9.5", "hasInstallScript": true, "license": "MIT", "dependencies": {
src/app/common/bookmark-zod-schemas.js+13 −13 modified@@ -38,7 +38,7 @@ const commonNetworkBookmarkProps = { username: z.string().optional().describe('Username'), password: z.string().optional().describe('Password'), description: z.string().optional().describe('Bookmark description'), - runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), + // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), startDirectoryRemote: z.string().optional().describe('Remote starting directory'), startDirectoryLocal: z.string().optional().describe('Local starting directory'), profile: z.string().optional().describe('Profile id'), @@ -67,9 +67,9 @@ const sshBookmarkSchema = { displayRaw: z.boolean().optional().describe('Display raw output, default is false'), encode: z.string().optional().describe('Charset, default is utf8'), envLang: z.string().optional().describe('ENV LANG, default is en_US.UTF-8'), - setEnv: z.string().optional().describe('Environment variables, format: KEY1=VALUE1 KEY2=VALUE2'), + // setEnv: z.string().optional().describe('Environment variables, format: KEY1=VALUE1 KEY2=VALUE2'), color: z.string().optional().describe('Tag color, like #000000'), - interactiveValues: z.string().optional().describe('Strings separated by newline'), + // interactiveValues: z.string().optional().describe('Strings separated by newline'), sshTunnels: z.array(sshTunnelSchema).optional().describe('SSH tunnel definitions'), connectionHoppings: z.array(connectionHoppingSchema).optional().describe('Connection hopping definitions') } @@ -95,21 +95,21 @@ const serialBookmarkSchema = { xon: z.boolean().optional().describe('XON flow control'), xoff: z.boolean().optional().describe('XOFF flow control'), xany: z.boolean().optional().describe('XANY flow control'), - description: z.string().optional().describe('Bookmark description'), - runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected') + description: z.string().optional().describe('Bookmark description') + // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected') } const localBookmarkSchema = { title: z.string().describe('Bookmark title'), description: z.string().optional().describe('Bookmark description'), - startDirectoryLocal: z.string().optional().describe('Local starting directory'), - runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), - execWindows: z.string().optional().describe('Windows exec path (overrides global setting)'), - execMac: z.string().optional().describe('Mac exec path (overrides global setting)'), - execLinux: z.string().optional().describe('Linux exec path (overrides global setting)'), - execWindowsArgs: z.array(z.string()).optional().describe('Windows exec arguments'), - execMacArgs: z.array(z.string()).optional().describe('Mac exec arguments'), - execLinuxArgs: z.array(z.string()).optional().describe('Linux exec arguments') + startDirectoryLocal: z.string().optional().describe('Local starting directory') + // runScripts: z.array(runScriptSchema).optional().describe('Run scripts after connected'), + // execWindows: z.string().optional().describe('Windows exec path (overrides global setting)'), + // execMac: z.string().optional().describe('Mac exec path (overrides global setting)'), + // execLinux: z.string().optional().describe('Linux exec path (overrides global setting)'), + // execWindowsArgs: z.array(z.string()).optional().describe('Windows exec arguments'), + // execMacArgs: z.array(z.string()).optional().describe('Mac exec arguments'), + // execLinuxArgs: z.array(z.string()).optional().describe('Linux exec arguments') } module.exports = {
src/client/components/main/main.jsx+1 −1 modified@@ -51,7 +51,7 @@ export default auto(function Index (props) { ipcOnEvent('open-about', store.openAbout) ipcOnEvent('new-ssh', store.onNewSsh) ipcOnEvent('add-tab-from-command-line', store.addTabFromCommandLine) - ipcOnEvent('open-tab', (e, parsed) => store.addTab(parsed)) + ipcOnEvent('open-tab', (e, parsed) => store.ipcOpenTab(parsed)) ipcOnEvent('openSettings', store.openSetting) ipcOnEvent('selectall', store.selectall) ipcOnEvent('focused', store.focus)
src/client/store/load-data.js+2 −2 modified@@ -100,7 +100,7 @@ export async function addTabFromCommandLine (store, opts) { (conf.username && conf.host) || conf.fromCmdLine ) { - store.addTab(conf) + store.ipcOpenTab(conf) } else if ( options.initFolder && !(store.config.onStartSessions || []).length && @@ -229,7 +229,7 @@ export default (Store) => { Store.prototype.checkPendingDeepLink = async function () { const pending = await window.pre.runGlobalAsync('getPendingDeepLink') if (pending) { - window.store.addTab(pending) + window.store.ipcOpenTab(pending) } } Store.prototype.parseQuickConnect = function (url) {
src/client/store/tab.js+20 −0 modified@@ -362,6 +362,26 @@ export default Store => { store.updateHistory(newTab) } + // Dangerous props that should not be accepted from IPC + const dangerousTabProps = [ + 'execLinux', + 'execMac', + 'execWindows', + 'execWindowsArgs', + 'execMacArgs', + 'execLinuxArgs', + 'setEnv', + 'runScripts', + 'interactiveValues' + ] + + Store.prototype.ipcOpenTab = function (parsed) { + const safeTab = Object.fromEntries( + Object.entries(parsed).filter(([key]) => !dangerousTabProps.includes(key)) + ) + return window.store.addTab(safeTab) + } + Store.prototype.clickNextTab = debounce(function () { window.store.clickBioTab(1) }, 100)
8a6a17951e96Security: Deep link support prop check
2 files changed · +18 −2
src/app/common/parse-quick-connect.js+9 −1 modified@@ -21,6 +21,12 @@ const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm'] +/** + * Deny list for opts keys - these are parsed from the URL itself + * and should not be overridable via the opts JSON parameter for safety + */ +const OPTS_DENY_LIST = ['type', 'host'] + /** * Default ports for each protocol */ @@ -393,6 +399,7 @@ function parseQuickConnect (str) { if (optsStr) { try { const extraOpts = JSON.parse(optsStr) + OPTS_DENY_LIST.forEach(key => delete extraOpts[key]) Object.assign(opts, extraOpts) } catch (err) { console.error('Failed to parse opts:', err) @@ -439,5 +446,6 @@ module.exports = { getDefaultPort, getSupportedProtocols, SUPPORTED_PROTOCOLS, - DEFAULT_PORTS + DEFAULT_PORTS, + OPTS_DENY_LIST }
src/client/common/parse-quick-connect.js+9 −1 modified@@ -21,6 +21,12 @@ const SUPPORTED_PROTOCOLS = ['ssh', 'telnet', 'vnc', 'rdp', 'spice', 'serial', 'ftp', 'http', 'https', 'electerm'] +/** + * Deny list for opts keys - these are parsed from the URL itself + * and should not be overridable via the opts JSON parameter for safety + */ +const OPTS_DENY_LIST = ['type', 'host'] + /** * Default ports for each protocol */ @@ -393,6 +399,7 @@ function parseQuickConnect (str) { if (optsStr) { try { const extraOpts = JSON.parse(optsStr) + OPTS_DENY_LIST.forEach(key => delete extraOpts[key]) Object.assign(opts, extraOpts) } catch (err) { console.error('Failed to parse opts:', err) @@ -439,5 +446,6 @@ export { getDefaultPort, getSupportedProtocols, SUPPORTED_PROTOCOLS, - DEFAULT_PORTS + DEFAULT_PORTS, + OPTS_DENY_LIST }
a79e06f4a1f0Security: Do not allow exec path with ".."
2 files changed · +7 −3
src/app/server/session-local.js+6 −2 modified@@ -24,13 +24,17 @@ class TerminalLocal extends TerminalBase { } = this.initOptions this.isLocal = true const { platform } = process - const exec = platform.startsWith('win') + const isWin = platform.startsWith('win') + const exec = isWin ? pathResolve( process.env.windir, execWindows ) : platform === 'darwin' ? execMac : execLinux - const arg = platform.startsWith('win') + if ((exec || '').includes('..')) { + return Promise.reject(new Error('execWindows should not contain ".."')) + } + const arg = isWin ? execWindowsArgs : platform === 'darwin' ? execMacArgs : execLinuxArgs const cwd = process.env[platform === 'win32' ? 'USERPROFILE' : 'HOME']
src/client/components/ai/ai-config.jsx+1 −1 modified@@ -6,7 +6,7 @@ import { Alert, Space } from 'antd' -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import Link from '../common/external-link' import AiCache from './ai-cache' import {
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
7- github.com/electerm/electerm/commit/8a6a17951e96d715f5a231532bbd8303fe208700nvdPatch
- github.com/electerm/electerm/commit/a79e06f4a1f0ac6376c3d2411ef4690fa0377742nvdPatch
- github.com/electerm/electerm/security/advisories/GHSA-mpm8-cx2p-626qnvdMitigationPatchVendor Advisory
- github.com/advisories/GHSA-mpm8-cx2p-626qghsaADVISORY
- github.com/electerm/electerm/releases/tag/v3.8.15nvdRelease Notes
- github.com/electerm/electerm/commit/0599e67069b00e376a2e962649aaad6096e63507nvd
- nvd.nist.gov/vuln/detail/CVE-2026-43944ghsa
News mentions
0No linked articles in our index yet.