Skip to content

Commit

Permalink
Fix command injection vulnerability and other bugs
Browse files Browse the repository at this point in the history
Change command execution approach, from 'child_process.exec' to
'child_process.spawn', removing the chance to send shell commands to
API. This increase code complexity, but fix a serious problem in return.

Improve logging, showing errors and interactions between child
processes.
Reduce google_speech and gtts verbosity.
Improve API error responses. Set charset to responses content.

Use 'gain' sox effect instead of 'overdrive'.
Add same sox effects used in google_speech to gtts.
Prepare gtts to accept a future option for slow reading.
  • Loading branch information
pedroetb committed Jul 14, 2019
1 parent 7bf5488 commit 29d9c25
Showing 1 changed file with 186 additions and 38 deletions.
224 changes: 186 additions & 38 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
}
}

Expand All @@ -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);
}

0 comments on commit 29d9c25

Please sign in to comment.