tl;dr
process.setuid
to drop privileges.setuid()
; be careful!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.