VYPR
Unrated severityNVD Advisory· Published May 19, 2026· Updated May 19, 2026

CVE-2026-37281

CVE-2026-37281

Description

An OS command injection vulnerability in the /stream-to-vlc Express route in hitarth-gg Zenshin before 2.7.0 allows remote attackers to execute arbitrary commands via the url parameter.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

OS command injection in Zenshin before 2.7.0 allows remote attackers to execute arbitrary commands via the url parameter in the /stream-to-vlc endpoint.

An OS command injection vulnerability (CWE-77) exists in the /stream-to-vlc Express route of Zenshin [2], an Electron-based anime streaming application. The vulnerability arises because the application constructs a shell command by concatenating the VLC executable path with the user-supplied url parameter and passes it to child_process.exec, which spawns a shell. An attacker can inject shell metacharacters such as & to execute arbitrary commands alongside the intended VLC launch [3].

Exploitation is straightforward due to the default network exposure. The Express server binds to 0.0.0.0, making the /stream-to-vlc endpoint accessible to any device on the local network [3]. An attacker can send a crafted HTTP GET request directly, e.g., curl -g 'http://[IP]:64621/stream-to-vlc?url=%22%20%26%20calc.exe', or use a malicious webpage to redirect the victim's browser to their local Zenshin instance, as demonstrated in a proof-of-concept video [3]. No authentication is required to trigger the endpoint.

Successful exploitation results in arbitrary command execution with the privileges of the Zenshin process. This can lead to full system compromise, including data exfiltration, malware installation, or lateral movement within the victim's network. Given the severity of OS command injection, this vulnerability is likely critical.

The issue has been addressed in version 2.7.0 [1]. The fix replaces child_process.exec with child_process.spawn, which does not invoke a shell, and validates the stream URL before processing. Additionally, the Express server is now bound to 127.0.0.1, reducing the attack surface to localhost-only. All users are strongly advised to update immediately; no workarounds beyond applying the patch are available.

AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
7d31c6edfbac

feat: replace `exec` with `spawn`, bind express server to 127.0.0.1

https://github.com/hitarth-gg/zenshinhitarth-ggMar 9, 2026via nvd-ref
6 files changed · +72 31
  • Electron/zenshin-electron/package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "zenshin",
    -  "version": "2.6.4",
    +  "version": "2.7.0",
       "description": "zenshin",
       "main": "./out/main/index.js",
       "author": "hitarth-gg",
    
  • Electron/zenshin-electron/src/main/index.js+40 25 modified
    @@ -3,7 +3,7 @@ import path, { join } from 'path'
     import { electronApp, optimizer, is } from '@electron-toolkit/utils'
     import icon from '../../resources/icon.ico?asset'
     import { Deeplink } from 'electron-deeplink'
    -import { exec } from 'child_process'
    +import { spawn } from 'child_process'
     import express from 'express'
     import cors from 'cors'
     import fs from 'fs'
    @@ -14,6 +14,7 @@ import WebSocket from 'ws'
     import announce from '../../common/announce'
     import Settings from './settings'
     import { mkdirp } from 'mkdirp'
    +import { cleanPath, validateStreamUrl } from '../renderer/src/utils/externalPlayer'
     
     let chalk
     import('chalk').then((module) => {
    @@ -159,14 +160,28 @@ function createWindow() {
         shell.openExternal(authUrl) // Open the OAuth URL in the default browser
       })
     
    -  ipcMain.on('open-vlc', (event, command) => {
    -    exec(command, (error) => {
    -      if (error) {
    -        dialog.showErrorBox(
    -          'Error launching External Player, make sure the path to .exe is correct. You can specify the correct path to it in the settings\n',
    -          error.message
    -        )
    -      }
    +  ipcMain.on('open-vlc', (event, player, url) => {
    +    const cleanedPlayerPath = cleanPath(player)
    +
    +    if (validateStreamUrl(url) === false) {
    +      dialog.showErrorBox(
    +        'Invalid Stream URL',
    +        'The provided stream URL is invalid. Please check the URL and try again.'
    +      )
    +      return
    +    }
    +
    +    const vlc = spawn(cleanedPlayerPath, [url], {
    +      shell: false,
    +      detached: true,
    +      stdio: 'ignore'
    +    })
    +
    +    vlc.on('error', (error) => {
    +      dialog.showErrorBox(
    +        'Error launching External Player, make sure the path to .exe is correct. You can specify the correct path to it in the settings\n',
    +        error.message
    +      )
         })
       })
     
    @@ -419,7 +434,7 @@ if (settings.get('downloadsFolderPath')) mkdirp(settings.get('downloadsFolderPat
     /* ------------------------------------------------------ */
     
     function startServer() {
    -  backendServer = app2.listen(settings.get('backendPort'), () => {
    +  backendServer = app2.listen(settings.get('backendPort'), '127.0.0.1', () => {
         console.log(`Server running at http://localhost:${settings.get('backendPort')}`)
       })
     
    @@ -856,24 +871,24 @@ app2.get('/details/:magnet', async (req, res) => {
     // import { get } from 'http'
     // import { fileURLToPath } from 'url'
     // Full path to VLC executable, change it as needed
    -const vlcPath = '"C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"' // Adjust this path as needed
    +// const vlcPath = '"C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe"' // Adjust this path as needed
     
    -app2.get('/stream-to-vlc', async (req, res) => {
    -  const { url, magnet } = req.query
    +// app2.get('/stream-to-vlc', async (req, res) => {
    +//   const { url, magnet } = req.query
     
    -  if (!url) {
    -    return res.status(400).send('URL is required')
    -  }
    -  const vlcCommand = `${vlcPath} "${url}"`
    +//   if (!url) {
    +//     return res.status(400).send('URL is required')
    +//   }
    +
    +//   spawn(vlcPath.replace(/"/g, ''), [url], {
    +//     shell: false,
    +//     detached: true,
    +//     stdio: 'ignore'
    +//   })
    +
    +//   res.send('VLC launched successfully')
    +// })
     
    -  exec(vlcCommand, (error) => {
    -    if (error) {
    -      console.error(`Error launching VLC: ${error.message}`)
    -      return res.status(500).send('Error launching VLC')
    -    }
    -    res.send('VLC launched successfully')
    -  })
    -})
     /* ------------------------------------------------------ */
     
     app2.delete('/remove/:magnet', async (req, res) => {
    
  • Electron/zenshin-electron/src/preload/index.js+1 1 modified
    @@ -7,7 +7,7 @@ const api = {
       maximize: () => ipcRenderer.send('maximize-window'),
       close: () => ipcRenderer.send('close-window'),
       oauth: (url) => ipcRenderer.send('oauth-login', url),
    -  openVlc: (url) => ipcRenderer.send('open-vlc', url),
    +  openVlc: (player, url) => ipcRenderer.send('open-vlc', player, url),
       openAnimePahe: (url) => ipcRenderer.send('open-animepahe', url),
       windowReload: () => ipcRenderer.send('reload-window'),
       changeBackendPort: (port) => ipcRenderer.send('change-backend-port', port),
    
  • Electron/zenshin-electron/src/renderer/src/pages/Player.jsx+3 3 modified
    @@ -195,7 +195,7 @@ export default function Player(query) {
           streamUrl: `http://localhost:${backendPort}/streamfile/${encodeURIComponent(magnetURI)}/${encodeURIComponent(episode)}`,
           ...loc.state
         }
    -    console.log(temp_obj);
    +    console.log(temp_obj)
     
         window.api.saveToSettings('currentAnime', temp_obj)
     
    @@ -222,7 +222,6 @@ export default function Player(query) {
         }
       }
     
    -
       const handleStreamVlc = async (episode) => {
         // save the data in the settings
         let temp_obj = {
    @@ -234,7 +233,8 @@ export default function Player(query) {
     
         try {
           window.api.openVlc(
    -        `${vlcPath} http://localhost:${backendPort}/streamfile/${encodeURIComponent(magnetURI)}/${encodeURIComponent(episode)}`
    +        vlcPath,
    +        `http://127.0.0.1:${backendPort}/streamfile/${encodeURIComponent(magnetURI)}/${encodeURIComponent(episode)}`
           )
         } catch (error) {
           console.error('Error streaming to VLC', error)
    
  • Electron/zenshin-electron/src/renderer/src/ui/AppLayout.jsx+1 1 modified
    @@ -39,7 +39,7 @@ export default function AppLayout({ props }) {
       /* ------------- CHECK LATEST GITHUB RELEASE ------------ */
       const owner = 'hitarth-gg' // Replace with the repository owner
       const repo = 'zenshin' // Replace with the repository name
    -  const currentVersion = 'v2.6.4' // Replace with the current version
    +  const currentVersion = 'v2.7.0' // Replace with the current version
     
       const getLatestRelease = async () => {
         try {
    
  • Electron/zenshin-electron/src/renderer/src/utils/externalPlayer.js+26 0 added
    @@ -0,0 +1,26 @@
    +export function cleanPath(path) {
    +  return path.replace(/^"(.*)"$/, '$1')
    +}
    +
    +export function validateStreamUrl(url) {
    +  try {
    +    const parsed = new URL(url)
    +
    +    // only allow HTTP
    +    if (parsed.protocol !== 'http:') return false
    +
    +    // only allow localhost
    +    if (parsed.hostname !== 'localhost' && parsed.hostname !== '127.0.0.1') {
    +      return false
    +    }
    +
    +    // cuz the backend serves the stream at /streamfile/:magnet/:episode
    +    if (!parsed.pathname.startsWith('/streamfile/')) {
    +      return false
    +    }
    +
    +    return true
    +  } catch {
    +    return false
    +  }
    +}
    

Vulnerability mechanics

Root cause

"The `/stream-to-vlc` Express route unsafely interpolated user-supplied `url` query parameter into a shell command string passed to `exec()`, enabling OS command injection."

Attack vector

An unauthenticated remote attacker sends an HTTP GET request to the `/stream-to-vlc` endpoint with a crafted `url` query parameter. The vulnerable code concatenated the user-supplied URL directly into a shell command string (`${vlcPath} "${url}"`) and executed it via `exec()` [patch_id=624735]. Because `exec()` spawns a shell, an attacker could inject arbitrary shell metacharacters (e.g., backticks, `$()`, semicolons) within the URL value to execute arbitrary OS commands. No authentication or special network position is required beyond reachability of the Express server.

Affected code

The vulnerable code was in `Electron/zenshin-electron/src/main/index.js` in the `/stream-to-vlc` Express route (lines ~856-876, now commented out). The route used `exec(vlcCommand)` where `vlcCommand` was built by string interpolation of the user-supplied `url` query parameter. The fix also touches `externalPlayer.js` (new validation functions), `Player.jsx` (IPC call signature), and `preload/index.js` (IPC bridge).

What the fix does

The patch removes the entire `/stream-to-vlc` Express route (commenting it out) and replaces the vulnerable `exec()` call with `spawn()` using `shell: false` in the IPC-based `open-vlc` handler [patch_id=624735]. Additionally, a new `validateStreamUrl()` function is introduced to restrict stream URLs to `http://localhost` or `http://127.0.0.1` paths beginning with `/streamfile/`, preventing arbitrary external or malicious URLs from being passed to the player. The Express server is also bound to `127.0.0.1` to reduce network exposure. These changes eliminate shell injection by avoiding string-based command construction and validating the URL before use.

Preconditions

  • networkThe Express server must be running and reachable by the attacker (pre-patch it was not bound to 127.0.0.1).
  • authNo authentication is required; the endpoint is publicly accessible.
  • inputThe attacker must be able to send an HTTP GET request to the /stream-to-vlc route with a url query parameter.

Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.