Avoid Shell Injection in Your Application

Shell injection is a form of injection attack where the target is the underlying operating system. More specifically, the attackers are focusing on the commands executed by the web application in the operating system layer. In Node.js this means commands executed through the child_process module, using exec, execFile, spawn, or fork. These commands can execute scripts on the operating system and can become a possible attack vector for code injection if the commands are incorrectly constructed with user input.

As with interpreter functions, shell commands are useful because they simplify the application logic by pushing certain tasks to external libraries. The two differences are the character set used and the execution environment. The attacker may not have access to runtime variables in a shell injection attack, but there are still plenty of ways to cause serious damage.

You might be thinking “Great. Another group of commands I simply won’t use.” As before, there are situations where shell commands drastically simplify the development or are required because of parallelism needs, for example, when we write an application that provides IP address information about URLs. We could look for a third-party module or write code to connect to the Domain Name System (DNS) and look up the information. Or we could use a command that comes with practically every operating system, host:

 'use strict'​;
 
 var​ express = require(​'express'​);
 var​ bodyParser = require(​'body-parser'​);
 var​ exec = require(​'child_process'​).exec;
 var​ app = express();
 
 var​ form = ​''​ +
 '<form method="POST" action="/host">'​ +
 '<input type="text" name="host" placeholder="host" />'​ +
 '<input type="submit" value="Get host" />'​ +
 '</form>'​;
 
 app.get(​'/'​, ​function​(req, res){
  res.send(form);
 });
 
 app.use(bodyParser.urlencoded({extended: ​false​}));
 
 app.post(​'/host'​, ​function​ (req, res) {
  exec(​'host '​ + req.body.host, ​function​ (err, stdout, stderr) {
 if​(err || stderr) {
  console.error(err || stderr);
  res.sendStatus(500);
 return​;
  }
  res.send(
 '<h3>Lookup for: '​ + req.body.host + ​'</h3>'​ +
 '<pre>'​ + stdout + ​'</pre>'​ +
  form
  );
  });
 });
 
 app.listen(3000);

And now a user could simply ask for information on Google.com, for example, and get a nice output, as shown in the figure.

images/google-host.png

Again, the problem is trusting the user to send valid input. What if the attacker sends something like google.com | cat /etc/shadow? If you don’t validate the user input, the attacker will probably be able to see the contents of the server’s /etc/shadow file, containing password information. Not a good result.

The first recommendation is to use execFile instead of exec when possible. The exec command uses the /bin/sh shell interpreter to run the command, which can be targeted by attackers to break out and launch other commands. execFile, however, executes the file directly, giving attackers a much smaller attack surface (limited by the file being executed). On the downside, you will lose some interoperability between environments and the ability to run complex commands with piping, but in turn your code will be more secure.

 var​ execFile = require(​'child_process'​).execFile;
 
 app.post(​'/host'​, ​function​ (req, res) {
  execFile(​'/usr/bin/host'​, [req.body.host], ​function​ (err, stdout, stderr) {
 // . . .
  });
 });

Another way to mitigate the attack surface is to whitelist and typecast the user-supplied variables when constructing shell commands. First, let’s whitelist our input and allow only certain characters:

 app.post(​'/host'​, ​function​ (req, res) {
 var​ host = req.body.host || ​''​;
 
 // Test for everything besides alphanumeric and . and -
 // Also test for starting . and -
 if​(host.match(​/^​​[​​-​​.]​​|​​[^​​a-zA-Z0-9​​-.]​​/​)) {
  res.status(400).send(​'Invalid input'​);
 return​;
  }
 
  execFile(​'/usr/bin/host'​, [host], ​function​ (err, stdout, stderr) {
 // ...
  });
 });

This fix is effective because it prevents users from creating other commands or manipulating them in unseemly ways. But whitelisting isn’t always possible, so instead we can limit run rights. We limit the rights the Node.js process has when executing the command by running it as a user with a limited set of rights. We can do this by providing corresponding uid or gid options.

Most Unix systems have a nobody user that we can use to run common services. We can set up our code to run the command as nobody by looking for the UID and setting it in the command options:

 var​ opts = {};
 app.post(​'/host'​, ​function​ (req, res) {
 // Add options specifying uid, which we asked from system
  execFile(​'/usr/bin/host'​, [req.body.host],
  opts, ​function​ (err, stdout, stderr) {
 if​(err || stderr) {
  console.error(err || stderr);
  res.sendStatus(500);
 return​;
  }
  res.send(
 '<h3>Lookup for: '​ + req.body.host + ​'</h3>'​ +
 '<pre>'​ + stdout + ​'</pre>'​ +
  form
  );
  });
 });
 // Look for the nobody user
 
 // NOTE:
 // On OSX this can cause an error because the UID of nobody
 // is a negative number (-1) represented by overflowing integer
 execFile(​'/usr/bin/id'​, [​'-u'​, ​'nobody'​], ​function​ (err, stdout, stderr) {
 if​(err || stderr) {
  console.error(err || stderr);
  process.exit(1);
  }
 
 // Set the uid in the options
  opts.uid = +stdout;
 
 // Start server
  console.log(​'Nobody is '​ + opts.uid);
  console.log(​'Listening on 3000'​);
  app.listen(3000);
 });

Now, an attacker trying to see the /etc/shadow file will see an error because it’s a restricted file and the nobody user doesn’t have access to it.

For best results, use the combination of all mitigation methods: execFile, limit access rights and possible inputs. As you can see, the defense methods for both shell injection and code injection follow the same principles.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.216.0