From d93780c9164be5f7e0b2f4094005378923659ce5 Mon Sep 17 00:00:00 2001 From: zachariah-mithani Date: Mon, 22 Jun 2026 23:50:58 -0400 Subject: [PATCH] fix: prevent command injection via file path in convertToWavType convertToWavType() interpolated the caller-supplied file path into a shell command string passed to shelljs.exec(): const command = `ffmpeg ... -i "${inputFilePath}" ... "${outputFilePath}"` shell.exec(command) The path is placed inside double quotes but never escaped, so a path that contains a double quote breaks out of the quoting. Because this is reached directly from the public nodewhisper(filePath, options) API (filePath is only existence-checked, not sanitized), an application that transcribes a user-influenced file name is exposed to arbitrary command execution. A file named: inp" ; touch PWNED ; ".mp3 runs: ffmpeg ... -i "inp" ; touch PWNED ; ".mp3" ... -> touch PWNED executes. Fix: run ffmpeg with execFileSync and a discrete argv array (no shell), so the file path is always treated as data and can never be parsed as shell syntax. Behaviour is unchanged for legitimate paths; the same error message is thrown on conversion failure. --- src/utils.ts | 50 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 53c4dc0..fb41021 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ +import { execFileSync } from 'child_process' import fs from 'fs' import path from 'path' -import shell from 'shelljs' import { Logger } from './types' export const checkIfFileExists = (filePath: string) => { @@ -39,6 +39,42 @@ async function isValidWavHeader(filePath: string): Promise { }) } +// Run ffmpeg to convert `inputFilePath` to a 16 kHz mono PCM WAV at `outputFilePath`. +// Arguments are passed as a discrete argv array with NO shell, so a path containing +// shell metacharacters (quotes, `;`, `$()`, backticks) is treated strictly as data and +// can never be interpreted as a command. Throws with the original error message on failure. +function runFfmpegConversion(inputFilePath: string, outputFilePath: string): void { + try { + execFileSync( + 'ffmpeg', + [ + '-nostats', + '-loglevel', + 'error', + '-y', + '-i', + inputFilePath, + '-ar', + '16000', + '-ac', + '1', + '-c:a', + 'pcm_s16le', + outputFilePath, + ], + { stdio: 'pipe' } + ) + } catch (err) { + const stderr = + err && typeof err === 'object' && 'stderr' in err && (err as { stderr?: unknown }).stderr + ? String((err as { stderr: unknown }).stderr) + : err instanceof Error + ? err.message + : 'unknown error' + throw new Error(`[Nodejs-whisper] Failed to convert audio file: ${stderr}`) + } +} + export const convertToWavType = async (inputFilePath: string, logger: Logger = console) => { const fileExtension = path.extname(inputFilePath).toLowerCase() @@ -55,11 +91,7 @@ export const convertToWavType = async (inputFilePath: string, logger: Logger = c // Use a temporary file to avoid overwrite conflicts const tempFile = inputFilePath + '.temp.wav' - const command = `ffmpeg -nostats -loglevel error -y -i "${inputFilePath}" -ar 16000 -ac 1 -c:a pcm_s16le "${tempFile}"` - const result = shell.exec(command) - if (result.code !== 0) { - throw new Error(`[Nodejs-whisper] Failed to convert audio file: ${result.stderr}`) - } + runFfmpegConversion(inputFilePath, tempFile) fs.renameSync(tempFile, inputFilePath) return inputFilePath @@ -73,11 +105,7 @@ export const convertToWavType = async (inputFilePath: string, logger: Logger = c logger.debug(`[Nodejs-whisper] Converting to a new WAV file: ${outputFilePath}`) - const command = `ffmpeg -nostats -loglevel error -y -i "${inputFilePath}" -ar 16000 -ac 1 -c:a pcm_s16le "${outputFilePath}"` - const result = shell.exec(command) - if (result.code !== 0) { - throw new Error(`[Nodejs-whisper] Failed to convert audio file: ${result.stderr}`) - } + runFfmpegConversion(inputFilePath, outputFilePath) return outputFilePath } }