Go Beyond

Written by Teran McKinney
/ About Me / Half-time Remote DevOps/Systems Engineer for $40,000 /

chroot() as non-root user

When working on pzqhttpd, I found out that chroot() only works as root. This is rather annoying, from a security standpoint.

There are any applications that are just in-and-out, after a couple file dsecriptors are open. Capsicum is excellent for these. Open up stdin, stdout, maybe a log file, cap_enter(), and start running real code beneath. If someone hijacks your code (or you have a very interesting bug), it'll be unable to open("/etc/passwd, O_RDWR) and start doing interesting things. It's pretty amazing sandboxing.

There's a couple drawbacks to Capsicum here. Linux doesn't support it. FreeBSD on ARM, at least out of the box, has Capsicum silently disabled. Even beyond that, for a web server the Capsicum rules become a little more complicated to allow for just open(), read(), and what not. I'm sure there's a way to do it that may be fairly obvious to most real coders, but it wasn't immediately apparent to be when writing pzqhttpd.

Also, I don't think Capsicum tries to reinvent the wheel. chroot() is what you want to lock your process into a directory. The interesting thing is, it only works as root. So it can be more secure to run something as root and have chroot(), than as your own user (which may have interesting data, itself), and not chroot().

Why is this?

The claim is that chroot() in an area with setuid binaries could allow the attacker to write out an absolute-pathed /etc/sudoers (or similar) and gain more privileges. I actually don't see too many easy or likely ways to do this, but I'm sure this has been thought of far more than I have.

But, it's unfortunate. This would be handy. The next best thing is chroot() + setuid() to a non-root user, ideally a constant UID unlikely to be picked by anything else. I do that in pzqhttpd, or well, I did, and I commented out, and I need to fix it. Still means you have to be root to have this security, which is counterintuitive.

I propose the following as a way to allow chroot() as a user on Linux, FreeBSD, etc:

When a process calls exec() on a binary, check if it's setuid and if the parent process is in a chroot. If is, deny it. I think this may be a few lines of code on most systems, should alleviate concerns of chroot() shenanigans, and make for easier security all around.

That said, I think there are still questionable things you can do in chroot(). Capsicum + chroot() is probably your best bet. All of this code should have varying degrees of sanity checking (chdir() if chroot() doesn't work and print a warning, etc), but opportunitistic security is better than none, especially as long as you know when you have and don't have it. If you don't plan on opening any files, /var/empty is probably the directory you want, but keep in mind that if you are root and not using Capsicum or something else, you'll be able to write there. If you are root and you are using chroot("/var/empty"), couple it with a setuid().

If this is or isn't possible, I'd appreciate if someone with more knowledge can chime in. I searched around a bit on the topic, but found little. There is also fakeroot, but I think this would be much cleaner.

Share on Voat.