Dropping privileges using process.setuid in node.js

When setting up a server, binding to a specific port can become problematic. Ports below 1024 are only bind-able by the root user, and alternatives often include using port redirection to a lesser privileged port. One way to get around this is through a similar tactic employed by the Apache web server, which is to bind to the port as root, then drop privileges so the service continues to run as a less privileged user. This helps prevent the case of servers trying to write to locations they shouldn’t, and helps to mitigate the effects of a server being exploited. First off we start with a simple HTTP server:

var http = require('http');
var server;

function StartServer() {
  console.log("Starting server...");
  // Initalizations such as reading the config file, etc.
  server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
  });
  server.listen(80, "127.0.0.1");
}

function StopServer(shutdown) {
  console.log("Stopping server...");
  // Other cleanup functions here
  if(server) {
    server.close();
    if(shutdown) {
      console.log("Exiting server process.");
      process.exit();
    }
  }
}

process.on("SIGHUP", function(){
  console.log("Received SIGHUP, restarting server...");
  StopServer();
  StartServer();
});

StartServer();

This simple server listens on port 80, and checks for the SIGHUP for restarting the server. We’ll go ahead and modify this code to accept arguments for the port to bind to:

if(process.argv.length > 2) {
  var port_number = process.argv[2];
}
else {
  console.error("Please provide a port to run on.");
  console.error("Usage: node restart_server.js port_number");
  process.exit(1);
}

var http = require('http');
var server;

function StartServer() {
  console.log("Starting server...");
  // Initalizations such as reading the config file, etc.
  server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
  });
  try {
    server.listen(port_number, "127.0.0.1");
  }
  catch(err) {
    console.error("Error: [%s] Call: [%s]", err.message, err.syscall);
    process.exit(1);
  }
}

function StopServer(shutdown) {
  console.log("Stopping server...");
  // Other cleanup functions here
  if(server) {
    server.close();
    if(shutdown) {
      console.log("Exiting server process.");
      process.exit();
    }
  }
}

process.on("SIGHUP", function(){
  console.log("Received SIGHUP, restarting server...");
  StopServer();
  StartServer();
});

StartServer();

So the main changes are:

if(process.argv.length > 2) {
  var port_number = process.argv[2];
}
else {
  console.error("Please provide a port to run on.");
  console.error("Usage: node restart_server.js port_number");
  process.exit(1);
}

We check against process.argv to see if there are any arguments passed in. When dealing with process.argv, we have to check if the arguments are greater than two, as process.argv[0] will be the node interpreter, and process.argv[1] will be the script file. This means that process.argv[2] and onwards are the actual arguments to the script. Let’s see what happens if we try to run this code as a regular user:

$ node restart_server.js 80
Starting server...
Error: [EACCES, Permission denied] Call: [bind]

Bind has failed because we don’t have the right permissions. Now, we’ll make two changes to be more friendly for the user. First off we’ll assume port 80 if no port is provided. Next we’ll let the user know that elevated privileges are required to bind to ports less than 1024:

// Quick helper function
function isRoot() { return process.getuid() == 0; }

if(process.argv.length > 2) {
  var port_number = process.argv[2];
}
else {
  var port_number = 80;
}

if(port_number < 1024 && !isRoot()) {
  console.error("Binding to ports less than 1024 requires root privileges.")
  process.exit(1);
}

First off a function is created for readability that checks if the current user is root, or userid 0. Then we check and see if a port was provided, and if not, set the port to 80. Finally, a check is run to see if we’re trying to bind to a privileged port, and if so alerts the user that root privileges are required. Let’s go ahead and test this out:

$ node restart_server.js
Binding to ports less than 1024 requires root privileges.

$ node restart_server.js 1024
Starting server...

Now, if we try and run as root, our server will bind properly:

$ sudo node restart_server.js 
Starting server...

...
tcp4       0      0  127.0.0.1.80           *.*                    LISTEN
...
root      1031   0.0  0.1  3038408  12044 s000  S+    4:22PM   0:00.10 node restart_server.js

Unfortunately it’s running as the root user, which is bad for the reasons explained at the beginning. So what we’ll do is have a secondary option to specify a user for the server to run after binding to a privileged port. So we change our parameter passing and bind code like so:

var process_user = '';
var port_number = 80;

// A single argument was passed in
if(process.argv.length == 3) {
  // Check if this is the port number or user to bind as
  if(parseInt(process.argv[2])) {
    port_number = process.argv[2];
  }
  else {
    process_user = process.argv[2];
  }
}
// port and user passed in
else if(process.argv.length == 4) {
  if(!parseInt(process.argv[2])) {
    console.error("Invalid port number: '%s'", process.argv[2]);
    process.exit(1);
  }

  process_user = process.argv[3];
}

if(port_number < 1024 && !isRoot()) {
  console.error("Binding to ports less than 1024 requires root privileges.")
  process.exit(1);
}
// Check to see if we're trying to run as root
// with no privileged user set.
else if (isRoot() && !process_user) {
  console.error("Please provide a non-privileged user to bind as.")
  console.error("Usage: node restart_server.js [port] user");
  process.exit(1);
}

var http = require('http');
var server;

function StartServer() {
  console.log("Starting server...");
  // Initalizations such as reading the config file, etc.
  server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
  });
  try {
    server.listen(port_number, "127.0.0.1");
    if(process_user) {
      process.setuid(process_user);
    }
  }
  catch(err) {
    console.error("Error: [%s] Call: [%s]", err.message, err.syscall);
    process.exit(1);
  }
}

First to note is some sanity checks on our arguments:

// A single argument was passed in
if(process.argv.length == 3) {
  // Check if this is the port number or user to bind as
  if(parseInt(process.argv[2])) {
    port_number = process.argv[2];
  }
  else {
    process_user = process.argv[2];
  }
}
// port and user passed in
else if(process.argv.length == 4) {
  if(!parseInt(process.argv[2])) {
    console.error("Invalid port number: '%s'", process.argv[2]);
    process.exit(1);
  }

  process_user = process.argv[3];
}

Two argument checks are run. The first is if we’re passing in only 1 argument. We need to see if the user is setting the port, or setting the user and leaving out the port instead. Next is if the user specifies the port and the user to run as. We also check that the port number is a valid integer. Then it’s time to check what user and port we’re binding as:

if(port_number < 1024 && !isRoot()) {
  console.error("Binding to ports less than 1024 requires root privileges.")
  process.exit(1);
}
// Check to see if we're trying to run as root
// with no privileged user set.
else if (isRoot() && !process_user) {
  console.error("Please provide a non-privileged user to bind as.")
  console.error("Usage: node restart_server.js [port] user");
  process.exit(1);
}

The main change here is to see if we’re running as root, and if so, if there’s an unprivileged user set. If there isn’t, we error out and notify the user. Finally a call to process.setuid is added immediately after binding:

  try {
    server.listen(port_number, "127.0.0.1");
    if(process_user) {
      process.setuid(process_user);
    }
  }
  catch(err) {
    console.error("Error: [%s] Call: [%s]", err.message, err.syscall);
    process.exit(1);
  }

It’s important to note that since we’re passing a non-numeric user id in, the setuid call is blocking. This is actually a good thing in this case, since we don’t want our server to do anything until privileges are dropped. The call to setuid is also added inside the try statement, since it could fail if we pass in an invalid user id. Let’s test the code out:

$ sudo node restart_server.js _www
Starting server...
..
tcp4       0      0  127.0.0.1.80           *.* 
..
_www      1092   0.0  0.1  3038412  12024 s000  S+    4:54PM   0:00.09 node restart_server.js _www
..
$ curl http://localhost/
Hello World

Our server works, and is running under the unprivileged user! However, we also need to sanity check a few other cases:

Unprivileged user binding to ports greater than or equal to 1024:

$ node restart_server.js 2222
Starting server...

Invalid user given:

$ sudo node restart_server.js user_that_does_not_exist
Starting server...
Error: [ENOENT, No such file or directory] Call: [getpwnam_r]

Invalid port given:

$ sudo node restart_server.js invalid _www
Invalid port number: 'invalid'

There, all our other edge cases have been checked. This concludes the guide on using process.setuid to help prevent running our service as root. Here is the full listing of the final code for reference:

// Quick helper function
function isRoot() { return process.getuid() == 0; }

var process_user = '';
var port_number = 80;

// A single argument was passed in
if(process.argv.length == 3) {
  // Check if this is the port number or user to bind as
  if(parseInt(process.argv[2])) {
    port_number = process.argv[2];
  }
  else {
    process_user = process.argv[2];
  }
}
// port and user passed in
else if(process.argv.length == 4) {
  if(!parseInt(process.argv[2])) {
    console.error("Invalid port number: '%s'", process.argv[2]);
    process.exit(1);
  }

  process_user = process.argv[3];
}

if(port_number < 1024 && !isRoot()) {
  console.error("Binding to ports less than 1024 requires root privileges.")
  process.exit(1);
}
// Check to see if we're trying to run as root
// with no privileged user set.
else if (isRoot() && !process_user) {
  console.error("Please provide a non-privileged user to bind as.")
  console.error("Usage: node restart_server.js [port] user");
  process.exit(1);
}

var http = require('http');
var server;

function StartServer() {
  console.log("Starting server...");
  // Initalizations such as reading the config file, etc.
  server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
  });
  try {
    server.listen(port_number, "127.0.0.1");
    if(process_user) {
      process.setuid(process_user);
    }
  }
  catch(err) {
    console.error("Error: [%s] Call: [%s]", err.message, err.syscall);
    process.exit(1);
  }
}

function StopServer(shutdown) {
  console.log("Stopping server...");
  // Other cleanup functions here
  if(server) {
    server.close();
    if(shutdown) {
      console.log("Exiting server process.");
      process.exit();
    }
  }
}

process.on("SIGHUP", function(){
  console.log("Received SIGHUP, restarting server...");
  StopServer();
  StartServer();
});

StartServer();

This entry was posted in IT and tagged , , , , . Bookmark the permalink.

4 Responses to Dropping privileges using process.setuid in node.js

  1. Benno says:

    Great article!

    One small thing, you should probably use server.on(‘error’, ….) rather than try {} catch {} around the ‘listen’ method. The listen method can be asynchronous so you might miss the bind exception in some cases.

  2. Benno says:

    On further thought, you can’t rely on the ‘bind’ syscall having been called when the listen() method returns. You probably can’t safely drop privileges until after the ‘listening’ event is called. It works fine for localhost in the current implementation, but if the listen implementation changed subtly you would end up dropping privileges too son.

    Of course, you really want to drop privileges before you start processing connection, which ideally means before the listen syscall is executed, so this is probably the best that can be done; the http library should really split the bind and listen methods to do this properly.

  3. Awesome post!! I’ve been researching and hacking at this for a day and a half looking for an elegant and tidy solution to this problem. Good Job!

  4. Hasan says:

    setuid should be done in setTimeout(function(){…}, 1) to allow the http server to complete the binding.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s