VYPR
High severity7.8GHSA Advisory· Published May 8, 2026· Updated May 8, 2026

CVE-2026-43943

CVE-2026-43943

Description

electerm is an open-sourced terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client. Prior to version 3.7.9, a code execution (RCE) vulnerability exists in electerm's SFTP open with system editor or "Edit with custom editor" feature. When a user opts to edit a file using open with system editor or open with a custom editor, the filename is passed directly into a command line without sanitization. A malicious actor controlling the SSH server or user OS can exploit this by crafting a filename containing shell metacharacters. If a victim subsequently attempts to edit this file, the injected commands are executed on their machine with the user's privileges. This could allow the attacker to run arbitrary code, install malware, or move laterally within the network. This issue has been patched in version 3.7.9.

Affected products

2

Patches

1
24ce7103e264

Improve open file function

https://github.com/electerm/electermZHAO XudongApr 26, 2026via nvd-ref
4 files changed · +144 14
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "electerm",
    -  "version": "3.7.6",
    +  "version": "3.7.8",
       "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.7.6",
    +  "version": "3.7.8",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
         "": {
           "name": "electerm",
    -      "version": "3.7.6",
    +      "version": "3.7.8",
           "hasInstallScript": true,
           "license": "MIT",
           "dependencies": {
    
  • src/app/lib/fs.js+54 11 modified
    @@ -5,9 +5,8 @@ const path = require('path')
     const { isWin, isMac, tempDir } = require('../common/runtime-constants')
     const uid = require('../common/uid')
     const { promisify } = require('util')
    -const execAsync = promisify(
    -  require('child_process').exec
    -)
    +const { exec, spawn } = require('child_process')
    +const execAsync = promisify(exec)
     const { getSizeCount, getSizeCountWin } = require('../common/get-folder-size-and-file-count.js')
     
     const ROOT_PATH = '/'
    @@ -49,6 +48,47 @@ const runWinCmd = (cmd) => {
       return execAsync(`powershell.exe -Command "${cmd}"`)
     }
     
    +function spawnDetachedCommand (command, args, options = {}) {
    +  return new Promise((resolve, reject) => {
    +    const child = spawn(command, args, {
    +      detached: false,
    +      stdio: ['ignore', 'ignore', 'pipe'],
    +      ...options
    +    })
    +    let stderr = ''
    +
    +    child.stderr.on('data', data => {
    +      stderr += data.toString()
    +    })
    +    child.on('error', reject)
    +
    +    let settled = false
    +    const settle = (err) => {
    +      if (settled) {
    +        return
    +      }
    +      settled = true
    +      clearTimeout(timer)
    +      child.unref()
    +      if (err) {
    +        reject(err)
    +      } else {
    +        resolve()
    +      }
    +    }
    +
    +    child.on('close', code => {
    +      if (code !== 0) {
    +        settle(new Error(stderr.trim() || `Command exited with code ${code}`))
    +      } else {
    +        settle(null)
    +      }
    +    })
    +
    +    const timer = setTimeout(() => settle(null), 5000)
    +  })
    +}
    +
     function getFolderSizeWin (folderPath) {
       return runWinCmd(
         `Get-ChildItem -Path "${folderPath}" -Recurse | Where-Object { ! $_.PSIsContainer } | Measure-Object -Property Length -Sum`
    @@ -109,16 +149,19 @@ const touch = (localFilePath) => {
      * @param {string} localFolderPath absolute path
      */
     const openFile = (localFilePath) => {
    -  let cmd
       if (isWin) {
    -    cmd = `Invoke-Item '${localFilePath}'`
    -    return runWinCmd(cmd)
    +    return spawnDetachedCommand('powershell.exe', [
    +      '-NoProfile',
    +      '-NonInteractive',
    +      '-Command',
    +      'Invoke-Item -LiteralPath $args[0]',
    +      '--',
    +      localFilePath
    +    ], {
    +      windowsHide: true
    +    })
       }
    -  cmd = (isMac
    -    ? 'open'
    -    : 'xdg-open') +
    -    ` "${localFilePath}"`
    -  return run(cmd)
    +  return spawnDetachedCommand(isMac ? 'open' : 'xdg-open', [localFilePath])
     }
     
     /**
    
  • test/unit/open-file.spec.js+87 0 added
    @@ -0,0 +1,87 @@
    +const test = require('node:test')
    +const assert = require('node:assert/strict')
    +
    +const childProcess = require('child_process')
    +const os = require('os')
    +
    +const originalSpawn = childProcess.spawn
    +const originalPlatform = os.platform
    +const originalArch = os.arch
    +const originalNodeEnv = process.env.NODE_ENV
    +
    +function loadFsForPlatform (platform) {
    +  os.platform = () => platform
    +  os.arch = () => 'x64'
    +  process.env.NODE_ENV = 'development'
    +  delete require.cache[require.resolve('../../src/app/lib/fs')]
    +  delete require.cache[require.resolve('../../src/app/common/runtime-constants')]
    +  return require('../../src/app/lib/fs').fsExport
    +}
    +
    +test.afterEach(() => {
    +  childProcess.spawn = originalSpawn
    +  os.platform = originalPlatform
    +  os.arch = originalArch
    +  process.env.NODE_ENV = originalNodeEnv
    +  delete require.cache[require.resolve('../../src/app/lib/fs')]
    +  delete require.cache[require.resolve('../../src/app/common/runtime-constants')]
    +})
    +
    +test('openFile passes malicious Windows filenames as a literal PowerShell argument', async () => {
    +  let spawnCall
    +  childProcess.spawn = (command, args, options) => {
    +    spawnCall = { command, args, options }
    +
    +    return {
    +      stderr: {
    +        on: () => {}
    +      },
    +      on: (event, handler) => {
    +        if (event === 'close') {
    +          process.nextTick(() => handler(0))
    +        }
    +      },
    +      unref: () => {}
    +    }
    +  }
    +
    +  const fsExport = loadFsForPlatform('win32')
    +  await fsExport.openFile("C:\\Temp\\poc';Start-Process calc;#.txt")
    +
    +  assert.equal(spawnCall.command, 'powershell.exe')
    +  assert.deepEqual(spawnCall.args, [
    +    '-NoProfile',
    +    '-NonInteractive',
    +    '-Command',
    +    'Invoke-Item -LiteralPath $args[0]',
    +    '--',
    +    "C:\\Temp\\poc';Start-Process calc;#.txt"
    +  ])
    +  assert.equal(spawnCall.options.windowsHide, true)
    +})
    +
    +test('openFile passes malicious Unix filenames directly to open command', async () => {
    +  let spawnCall
    +  childProcess.spawn = (command, args, options) => {
    +    spawnCall = { command, args, options }
    +
    +    return {
    +      stderr: {
    +        on: () => {}
    +      },
    +      on: (event, handler) => {
    +        if (event === 'close') {
    +          process.nextTick(() => handler(0))
    +        }
    +      },
    +      unref: () => {}
    +    }
    +  }
    +
    +  const fsExport = loadFsForPlatform('darwin')
    +  await fsExport.openFile('/tmp/poc";touch /tmp/pwn;#.txt')
    +
    +  assert.equal(spawnCall.command, 'open')
    +  assert.deepEqual(spawnCall.args, ['/tmp/poc";touch /tmp/pwn;#.txt'])
    +  assert.equal(spawnCall.options.detached, false)
    +})
    

Vulnerability mechanics

AI mechanics synthesis has not run for this CVE yet.

References

5

News mentions

0

No linked articles in our index yet.