pedroetb tts-api app.js onSpeechDone os command injection
Description
Critical OS command injection in pedroetb/tts-api up to 2.1.4 allows remote code execution; upgrade to v2.2.0 to fix.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Critical OS command injection in pedroetb/tts-api up to 2.1.4 allows remote code execution; upgrade to v2.2.0 to fix.
Vulnerability
CVE-2019-25158 is a critical OS command injection vulnerability in the onSpeechDone function of app.js in pedroetb/tts-api versions up to and including 2.1.4. The function constructs shell commands by concatenating user-supplied parameters (such as text, language, and speed) without proper sanitization or escaping, allowing an attacker to inject arbitrary OS commands [1][2].
Exploitation
The vulnerability is exploitable by sending a crafted HTTP POST request to the TTS API endpoint. The attacker does not require authentication, as the API is typically exposed without access controls. By manipulating the textToSpeech or other fields, the attacker can break out of the intended command and execute arbitrary system commands on the server [2][3].
Impact
Successful exploitation leads to remote code execution with the privileges of the tts-api process, which often runs as a non-root user but can still compromise the host system, exfiltrate data, or launch further attacks [1][3].
Mitigation
The vendor fixed the issue in version 2.2.0 by rewriting the command construction to use parameterized arrays instead of string concatenation, effectively preventing injection [2]. Users should upgrade immediately. Alternatively, the patch can be applied manually (commit 29d9c25415911ea2f8b6de247cb5c4607d13d434) [2][4].
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
tts-apinpm | < 2.2.0 | 2.2.0 |
Affected products
2Patches
129d9c2541591Fix command injection vulnerability and other bugs
1 file changed · +186 −38
app.js+186 −38 modified@@ -20,8 +20,7 @@ server.set('view engine', 'pug') .listen(port, function() { - var port = this.address().port; - console.log('Listening at port', port); + console.log('Listening at port', this.address().port); }); function renderForm(req, res) { @@ -33,17 +32,16 @@ function renderForm(req, res) { function processData(req, res) { var body = req.body, - exec = childProcess.exec, - cmd = getCmdWithArgs(body), - cbk = onSpeechDone.bind(this, { + cmdWithArgs = getCmdWithArgs(body) || {}, + httpArgs = { res: res, fields: body - }); + }; - if (!cmd) { - cbk('Empty command generated'); + if (cmdWithArgs instanceof Array) { + runSpeechProcessChain(cmdWithArgs, httpArgs); } else { - exec(cmd, cbk); + runLastSpeechProcess(cmdWithArgs, httpArgs); } } @@ -60,71 +58,221 @@ function getCmdWithArgs(fields) { } else if (voice === 'espeak') { return getEspeakCmdWithArgs(fields); } - - return ''; } function getGoogleSpeechCmdWithArgs(fields) { var text = fields.textToSpeech, language = fields.language, - speed = fields.speed, - - cmd = 'google_speech' + ' -l ' + language + ' \"' + text + '\"' + ' -e overdrive 10 speed ' + speed; - - return cmd; + speed = fields.speed; + + return { + cmd: 'google_speech', + args: [ + '-v', 'warning', + '-l', language, + text, + '-e', + 'gain', '4', + 'speed', speed + ] + }; } function getGttsCmdWithArgs(fields) { var text = fields.textToSpeech, language = fields.language, speed = fields.speed, - speedParam = speed ? ' -s' : '', - - cmd = 'gtts-cli' + ' -l ' + language + speedParam + ' --nocheck \"' + text + '\"' + ' | play -t mp3 -'; - - return cmd; + slowSpeed = fields.slowSpeed ? '-s' : ''; + + return [{ + cmd: 'gtts-cli', + args: [ + '-l', language, + '--nocheck', + slowSpeed, + text + ] + },{ + cmd: 'play', + args: [ + '-q', + '-t', 'mp3', + '-', + 'gain', '4', + 'speed', speed + ] + }]; } function getFestivalCmdWithArgs(fields) { var text = fields.textToSpeech, - language = fields.language, - - cmd = 'echo "' + text + '" | festival' + ' --tts --heap 1000000 --language ' + language; - - return cmd; + language = fields.language; + + return [{ + cmd: 'echo', + args: [ + text + ] + },{ + cmd: 'festival', + args: [ + '--tts', + '--language', language, + '--heap', '1000000' + ] + }]; } function getEspeakCmdWithArgs(fields) { var text = fields.textToSpeech, language = fields.language, voiceCode = '+f4', + voice = language + voiceCode, speed = Math.floor(fields.speed * 150), - pitch = '70', + pitch = '70'; + + return { + cmd: 'espeak', + args: [ + '-v', voice, + '-s', speed, + '-p', pitch, + text + ] + }; +} + +function runLastSpeechProcess(cmdWithArgs, httpArgs) { + + var speechProcess = runSpeechProcess(cmdWithArgs); - cmd = 'espeak' + ' -v' + language + voiceCode + ' -s ' + speed + ' -p ' + pitch + ' \"' + text + '\"'; + speechProcess.on('error', onLastSpeechError.bind(this, httpArgs)); + speechProcess.on('close', onLastSpeechClose); + speechProcess.on('exit', onLastSpeechExit.bind(this, httpArgs)); - return cmd; + return speechProcess; } -function onSpeechDone(args, err, stdout, stderr) { +function runSpeechProcess(cmdWithArgs) { - var res = args.res, - fields = args.fields; + var newProcess = childProcess.spawn(cmdWithArgs.cmd, cmdWithArgs.args); + + newProcess.stderr.on('data', onSpeechStandardError); + + return newProcess; +} + +function onSpeechStandardError(buffer) { + + console.error('[stderr]:', buffer.toString('utf8')); +} + +function runSpeechProcessChain(cmdWithArgs, httpArgs) { + + var speechProcs = {}; + + for (var i = 0; i < cmdWithArgs.length; i++) { + if (i !== cmdWithArgs.length - 1) { + var getNextProcessCbk = getNextSpeechProcess.bind(speechProcs, i + 1); + speechProcs[i] = runIntermediateSpeechProcess(cmdWithArgs[i], getNextProcessCbk); + } else { + speechProcs[i] = runLastSpeechProcess(cmdWithArgs[i], httpArgs); + } + } +} + +function runIntermediateSpeechProcess(cmdWithArgs, procArgs) { + + var speechProcess = runSpeechProcess(cmdWithArgs); + + speechProcess.stdout.on('data', onIntermediateSpeechStandardOutput.bind(this, procArgs)); + speechProcess.on('error', onIntermediateSpeechError); + speechProcess.on('close', onIntermediateSpeechClose.bind(this, procArgs)); + + return speechProcess; +} + +function getNextSpeechProcess(nextIndex) { + + return this[nextIndex]; +} + +function onIntermediateSpeechStandardOutput(getNextProc, data) { + + var nextSpeechProcess = getNextProc(), + inputStream = nextSpeechProcess.stdin; + + if (inputStream.writable) { + inputStream.write(data); + } +} + +function onIntermediateSpeechClose(getNextProc, code) { + + var nextSpeechProcess = getNextProc(), + inputStream = nextSpeechProcess.stdin; + + if (code) { + console.error('[intermediate exit code]:', code); + } + + inputStream.end(); +} + +function onIntermediateSpeechError(err) { + + console.error('[intermediate error]:', util.inspect(err)); +} + +function onLastSpeechClose(code) { + + if (code) { + console.error('[exit code]:', code); + } +} + +function onLastSpeechExit(args, err) { + + var res = args.res; if (!err) { res.end(); - return; + } else { + handleSpeechError(args, err); } +} + +function onLastSpeechError(args, err) { + + handleSpeechError(args, err); +} + +function handleSpeechError(args, err) { + + var res = args.res, + fields = args.fields, + errorHeaderMessage = '----[error]----', + dataHeaderMessage = '-----[data]-----', + inspectedError = util.inspect(err), + inspectedFields = util.inspect(fields); res.writeHead(500, { - 'content-type': 'text/plain' + 'Content-Type': 'text/plain; charset=utf-8' }); - res.write('error:\n\n'); - res.write(util.inspect(err) + '\n\n'); - res.write('received data:\n\n'); - res.end(util.inspect(fields)); + + res.write(errorHeaderMessage + '\n'); + res.write(inspectedError + '\n'); + res.write(dataHeaderMessage + '\n'); + res.write(inspectedFields + '\n'); + + res.end(); + + console.error(errorHeaderMessage); + console.error(inspectedError); + console.error(dataHeaderMessage); + console.error(inspectedFields); }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/pedroetb/tts-api/commit/29d9c25415911ea2f8b6de247cb5c4607d13d434ghsapatchWEB
- github.com/pedroetb/tts-api/releases/tag/v2.2.0ghsapatchWEB
- github.com/advisories/GHSA-jx6q-fq9h-6g7qghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2019-25158ghsaADVISORY
- vuldb.comghsasignaturepermissions-requiredWEB
- vuldb.comghsavdb-entrytechnical-descriptionWEB
News mentions
0No linked articles in our index yet.