Safely dropping privileges in node.js

Tue, 09 Aug 2011 15:12:25 +0000
node.js setuid security

tl;dr

So, you want to run some kind of TCP server, and you’d like to run it on one of those fancy ports with a number less-than 1024. Well, unfortunately you got to be root to bind to a low-numbered port. Of course, we don’t want to run our network server as root, because that would be, well, really silly, wouldn’t it! Luckily, POSIX gives us a simple way of breaking this little problem. You start your program running with root privileges, grab all the resources you need, and then drop back to running as an unprivileged user using the setuid syscall.

Now, if you are writing a network server you probably know the drill, you create a socket(), then you bind(), and then you starts to listen() for connections, occasionally calling accept() when you decide you want to actually do something with an incoming request. So, the question is, at which point do you drop the privileges? Well, the important part is that you need privileges to bind(), but once you have bound to an address and port, you no longer need root privileges. So ideally, you call setuid() after you bind(). You want to get this right. Drop privileges too early and you can’t correctly bind to the address, drop too late and you unnecessarily expose yourself to potential exploits.

Now, if you are doing something in normal synchronous programming you would do something like:

fd = socket(...)
bind(fd, ...)
setuid(...)
listen(fd, ...)

But good luck on things being so simple in node.js. In my last post I described these semi-asynchronous functions, which you probably thought was just a bit of an academic exercise. Well, it turns out that, depending on the arguments, the listen method behaves in this semi-asynchronous manner.

Specifically, when the listen function returns, the bind() operation has completed, but the listen() operation hasn’t. Which means that calling process.setuid() immediately after server.listen() will end up dropping privileges at the ideal time.

This technique is explained in this excellent post on the subject. However, I’m not 100% satisfied with this solution. My unease with this approach comes down to the fact that there is no documented guarantee that the bind() must have occurred when the function returns, it could change in the next version. In fact, depending on the arguments passed to listen, it may not happen that way. If instead of using an IP address to specify the local address to bind to, you use a domain name, then an asynchronous DNS lookup occurs before the call to bind(), which means that when server.listen() returns the bind call has not yet happened, if you drop the privileges at this point then you will hit an exception later when the bind() happens. Of course, specifying the local address to which your server binds using a DNS name is a little bits silly in the first place, but that is another matter.

So, if we can’t rely on the bind() having occurred when server.listen() returns then the only other option is to call setuid in the listen callback function. This is probably a reasonable approach, but it does mean that we hold privileges longer than strictly necessary. In this case, there probably isn’t really very much that happens between the bind() call and when the listen event triggers, so it doesn’t really matter, but I’d still like to find a solution that avoids both of these problems.

Thankfully, node.js is pretty flexible and provides a listenFD() method that we can take advantage of. This lets us set up our own socket first, with whatever exact timings we want, and then let the class know about the socket we created.

It turns out that writing function to create an appropriate socket isn’t too hard as most of the low-level functions are available if you know where to look. So I present you with safeListen

function safeListen(server, port, address, user) {
    var ip_ver = net_binding.isIP(address)
    var fd
    var type

    switch (ip_ver) {
    case 4:
	type = 'tcp4'
	break
    case 6:
	type = 'tcp6'
	break
    default:
	throw new Error("Address must be a valid IPv4 or IPv6 address.")
    }

    fd = net_binding.socket(type)

    net_binding.bind(fd, port, address)

    if (user) {
	process.setuid(user)
    }

    net_binding.listen(fd, server._backlog || 128)

    /* Following the net.js listen implementation we do this in the
     nextTick so that people potentially have time to register
     'listening' listeners. */
    process.nextTick(function() {
	server.listenFD(fd, type)
    })
}

Instead of using server.listen(address, port) use safeListen(server, address, port, user). If you like monkey patching you can probably attach the function as a method to the server object and then make the call look like server.safeListen(address, port, user). This function essentially does the same thing as listen but if a user argument is specified, it will call setuid to drop privileges after calling bind(). The main limitation compared to the normal listen() method is that the address must be specified, and must be an IP address, rather than a hostname.

blog comments powered by Disqus