50

In nodejs, the only way to execute external commands is via sys.exec(cmd). I'd like to call an external command and give it data via stdin. In nodejs there does yet not appear to be a way to open a command and then push data to it (only to exec and receive its standard+error outputs), so it appears the only way I've got to do this right now is via a single string command such as:

var dangerStr = "bad stuff here";
sys.exec("echo '" + dangerStr + "' | somecommand");

Most answers to questions like this have focused on either regex which doesn't work for me in nodejs (which uses Google's V8 Javascript engine) or native features from other languages like Python.

I'd like to escape dangerStr so that it's safe to compose an exec string like the one above. If it helps, dangerStr will contain JSON data.

mikemaccana
  • 81,787
  • 73
  • 317
  • 396
Maciek
  • 3,192
  • 6
  • 25
  • 35
  • 11
    For Bourne-type shells you can use the following algorithm to safely escape strings: 1) replace all occurrences of single quote (') with the four character sequence single quote, backslash, single quote, single quote ('\'') 2) add a additional single quote to the beginning and of the end of the modified string. Leading and trailing single quotes are not encoded perfectly efficiently, but it still works—' becomes ''\''' when it could be just \'. – Chris Johnsen Nov 22 '09 at 23:48
  • For clarification: it took me a little while to understand @ChrisJohnsen's advice but it checks out. If you want `don't do that` on the shell, do `echo 'don'\''t do that'` to produce `don't do that`. – mikemaccana Jan 10 '15 at 20:16
  • In python, it's implemented here https://github.com/python/cpython/blob/ad83fde75463dad2df878ff264f52436eb48bc6b/Lib/subprocess.py#L533 – Boris Mar 12 '21 at 05:58

7 Answers7

37

This is what I use:

var escapeShell = function(cmd) {
  return '"'+cmd.replace(/(["'$`\\])/g,'\\$1')+'"';
};
lumio
  • 6,606
  • 3
  • 30
  • 50
Sylvain Zimmer
  • 411
  • 4
  • 3
  • What is doing the first $? In the api reference says that matches only if is the end of the string, but it is not this case. I tried the string "/aPath'/with/qu'otes'" (notice it ends with single quote + double), with $ and without $. The outcome is the same. – David Torres Jun 16 '15 at 11:12
  • 1
    @DavidTorres `abc$abc` becomes `abc\$abc` – Konstantin Tarkus Sep 08 '15 at 07:13
  • 1
    No need to esacpe `$` if use apostrophe `'` – Alex Yaroshevich Jan 26 '17 at 13:21
  • Why do the other quote types need escaping at all? Surely only the type of quote being used poses a danger. – Nigel B. Peck May 20 '17 at 21:23
  • 4
    This seems wrong: it escapes `foo bar` as `"foo\ bar"`, which will be parsed as `foo\ bar`. – Anders Kaseorg Jul 13 '17 at 21:59
  • @AlexYaroshevich If you use apostrophe, you cannot use backslash encoding at all as backslash is an ordinary character within apostrophes. How would you then escape an apostrophe character in the string? Right, you can't. That's why you should not use apostrophe for strings that may contain an apostrophe. – Mecki Oct 26 '18 at 09:23
  • 1
    Quoting whitespace (`\s`) is unnecessary if you wrap everything in double quotes as that will automatically retain all whitespace. You would only need to do that if there were no quotes around the string otherwise space is seen as an argument separator by the shell. Also you are not quoting space correctly as you turn a tab into `\ ` but the correct form would be `\t`. – Mecki Oct 26 '18 at 09:28
  • 5
    This seems like an invitation for command injection and weird edge case issues. – Cully Jan 22 '19 at 23:05
22

If you need simple (yet correct) solution you can use this:

function escapeShellArg (arg) {
    return `'${arg.replace(/'/g, `'\\''`)}'`;
}

So your string will be simply escaped with single quotes as Chris Johnsen mentioned.

echo 'John'\''s phone';

It works in bash because of strong quoting, feels like it also works in fish, but does not work in zsh and sh.

If you have bash your can run your script in sh or zsh with 'bash -c \'' + escape('all-the-rest-escaped') + '\''.

But actually... node.js will escape all needed characters for you:

var child = require('child_process')
  .spawn('echo', ['`echo 1`;"echo $SSH_TTY;\'\\0{0..5}']);

child.stdout.on('data', function (data) {
  console.log('stdout: ' + data);
});

child.stderr.on('data', function (data) {
  console.log('stderr: ' + data);
});

this block of code will execute:

echo '`echo 1`;"echo $SSH_TTY;'\''\\0{0..5}'

and will output:

stdout: `echo 1`;"echo $SSH_TTY;\'\\0{0..5}

or some error.

Take a look at http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options

By the way simple solution to run a bunch of commands is:

require('child_process')
  .spawn('sh', ['-c', [
    'cd all/your/commands',
    'ls here',
    'echo "and even" > more'
  ].join('; ')]);

Have a nice day!

Alex Yaroshevich
  • 680
  • 8
  • 18
19

You should never rely on escaping unknown input going to a shell parameter - there will almost always be some edge-case that you haven't thought of that allows the user to execute arbitrary code on your server.

Node has support for calling a command and passing each argument separately, with no escaping required. This is the safest way to do it:

const { spawn } = require('child_process');
// Note that the arguments are in an array, not using string interpolation
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.log(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

The documentation is here

Will Richardson
  • 7,012
  • 6
  • 39
  • 50
2

I second the opinion of Will, whenever possible you should avoid escaping by hand and prefer spawn.

However, in the case that escaping is unavoidable, for example if you need to use exec or you are executing a command through ssh. Then you can use base64 to pass safe characters to bash and rely on bash to escape the unknown.

const dangerStr = 'bad stuff here'
// base64 has safe characters [A-Za-z=0-9+/]
const dangerBase64 = btoa(dangerStr)

sys.exec(`echo "$(echo ${dangerBase64} | base64 -d)" | somecommand`)

The explanation is the following:

dangerBase64 is unknown but it does not contain unsafe characters in bash. Hence echo ${dangerBase64} will output what we want.

Finally the double quote around $(echo ${dangerBase64} | base64 -d) escape the actual value passed by the user inside bash, which is safe and has the same value that the user wanted.

Gabriel Furstenheim
  • 1,941
  • 19
  • 18
0

If you are building you own software, you can encode the command to base64 or hex format then decode the arguments from the program.

For my Nodejs applications I use.

var base64_encode = exports.base64_encode = function(non_base64_string){
    return Buffer.from(non_base64_string).toString('base64');
}


var base64_decode = exports.base64_decode = function(base64_string){
    return Buffer.from(base64_string, 'base64').toString('ascii')
}

So when I run a base64 encoded command like this

webman grep --search "aW5jbHVkZV9vbmNlICRfU0VSVkVSWyJET0NVTUVOVF9ST09UIl0uIi9zZXR0aW5ncy5waHAiOw==" --replacement "JGRvY3VtZW50X3Jvb3QgPSBfX0RJUl9fO3doaWxlKHRydWUpe2lmIChmaWxlX2V4aXN0cygkZG9jdW1lbnRfcm9vdC4iL3NldHRpbmdzLmpzb24iKSl7YnJlYWs7fWVsc2V7JGRvY3VtZW50X3Jvb3Q9ZGlybmFtZSgkZG9jdW1lbnRfcm9vdCk7fX08bmV3bGluZT5pbmNsdWRlX29uY2UgJGRvY3VtZW50X3Jvb3QuIi9zZXR0aW5ncy5waHAiOw=="

I can get the arguments search and replacement arguments without stress using base64_decode

-1

If you also need to deal with special character (line-breaks etc.) you can do it this way:

str = JSON.stringify(str)
    .replace(/^"|"$/g,'') //remove JSON-string double quotes
    .replace(/'/g, '\'"\'"\'') //escape single quotes the ugly bash way

This assumes you use Bash's strong-quoting via single-quotes) and the receiver can understand JSON's C-like escaping.

MicMro
  • 107
  • 1
  • 3
-14

There is a way to write to an external command: process.createChildProcess (documentation) returns an object with a write method. createChildProcess isn't as convenient though, because it doesn't buffer stdout and stderr, so you will need event handlers to read the output in chunks.

var stdout = "", stderr = "";
var child = process.createChildProcess("someCommand");

child.addListener("output", function (data) {
    if (data !== null) {
        stdout += data;
    }
});
child.addListener("error", function (data) {
    if (data !== null) {
        stderr += data;
    }
});
child.addListener("exit", function (code) {
    if (code === 0) {
        sys.puts(stdout);
    }
    else {
        // error
    }
});

child.write("This goes to someCommand's stdin.");
Matthew Crumley
  • 95,375
  • 24
  • 103
  • 125
  • Interesting.. Thanks! BTW, The new URI is http://nodejs.org/api/child_process.html – grilix May 16 '12 at 19:22
  • 53
    You don't explain anything about escaping arguments. – Will Nov 29 '12 at 06:32
  • 3
    This does not answer what the OP asked about, which is escaping arguments. The use of `echo` was just an example. The OP is not just trying to pipe a string to a command. They're trying to use an arbitrary string at the command-line. – nonrectangular Oct 09 '13 at 19:03
  • 4
    It doesn't answer the question in the title, but this solves the problem by avoiding the issue completely. The OP accepted the answer, so apparently it helped. – Matthew Crumley Oct 09 '13 at 21:18