Why pledge(2) or, how I learned to love web application sandboxing
learnbchs.org
learnbchs.org
No. Do not write your webapp in C. I'm serious here. Write it in Python, write it in TCL, write it in Lisp. But write it an environment that is at least semi-manged.
FFIs are pretty good. You can call pledge from your app. But don't write your app in C. If you're doing anything serious, it's a bad idea.
But in reality writing applications in a hosted environment/virtual machine is more performant than writing it in C and locking it down with operating system features. And of course more secure.
The author's efforts would be better spent on improving the performance of OS-based security (processes, filesystems), than attempting to use that security as it stands. There are all kinds of tricks that could be implemented to improve things...
On top of that, the CPU throughput overhead of using a compiled managed language is typically less than a factor of 2, which, combined with the fact that the majority of web applications are not compute bound means the number of webapps that it would make any sense to write in C is nearly zero.
Pretty much all pithy advice (such as "Don't write your webapps in C") has an implicit exception "Unless you fully understand why I made this recommendation and have good reasons for why they don't apply."
That exception is not made explicit because 1) It would make the advice less pithy and 2) Then those who would most benefit from the advice convince themselves that it doesn't apply to them.
So...I think you're missing a few pieces of the puzzle with this statement. The statement is a truism that is right almost always, but it has exceptions, and this is one of those exceptions.
CGI is slow when startup time for your app is slow, because of a VM, because of the library loading overhead, because of environment setup, because of some thing that isn't really the fault of CGI per se. CGI can be fast, or at least fast enough, if startup time for your app is very fast. An app written in C, statically linked, could potentially start and respond faster than many dynamic language apps could even when running in an app server. C is fast. C is small. C is also particularly error-prone, would be a little verbose for web apps, and does not have a good ecosystem of support libraries for web apps, but even if running it without an app server in a CGI mode it could be fast enough for most deployments.
The CGI interface is not why CGI apps are slow. CGI apps are slow because startup of many modern language environments is slow. C is a language that doesn't have to be slow in that context. (Though, it could be faster still by using an FCGI or other app server model and having your C web app running all the time, so it just has to answer requests over a socket or whatever...though async programming in C is also hard to get right for inexperienced programmers.)
Do you write command line apps with CHICKEN? Are they super fast to start, like instant (comparable to something written in C or small Perl scripts)? If so, then CGI could probably be fast enough for some use cases. If you can perceive the startup time, then no, it'd be kinda sucky when used with CGI.
Not a VM: it's compiled. It needs to bind to libchicken, and it needs to do some things that native C programs wouldn't.
Also, you wouldn't need a VM for macros: they're a compile-time feature, and don't even exist at runtime.
Yeah, it's not humanly noticable.
on the same system:
/bin/true starts up in 0.5ms
/usr/bin/env perl starts up in 2.4ms
/usr/bin/perl starts up in 1.8ms
If you want to run your own benchmarks, just use time and something like this small program (I ran with 1000 iterations):
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
int iter=atoi(argv[1]);
for(i=0;i<iter;++i) {
pid_t pid=fork();
if(pid) {
waitpid(pid,NULL,0);
}
else {
execl(argv[2],argv[2],NULL);
}
}
return 0;
}Not too sure about that -- Erlang/OTP-based web applications have very good performance (and transparent scaling) with a fraction of the code of a hand-rolled C-based web application. I honestly think web applications are a place where everything but assembly is better than C.
The GNU 'echo' program is pretty significant, in the sense it's installed widely, and used widely. What sort of vulnerability exists?
Recommending against C for web apps is wise in this context.
What a non sequitur. That I do not like being told what to (not) use has no bearing whatsoever on whether I can like or dislike the thing that someone somewhere is advocating for or against.
Yes, I use C. Is the programming language telling me to use this or that? No, I am telling it to do this or that.
And no, I am not using it because somebody told me to. I am using it because (as I said above), I like it, I enjoy using it, I am good with it, and feel like it's a good fit for the values I hold. Opinions shared by other people may have affected my perception, but I am not using the language because somebody tells me to.
But if it really were the case that it is literally impossible to write secure code in C (you'd have to prove it), then my web application should be the least of anyone's worries. Because they are using millions of lines of kernel and library code written in C or C++ to access my web application, which again is hosted on millions of lines of kernel, daemon and library code (including all the crypto) written in C or C++.
And the hypothetical alternative language would likely be implemented in C or C++. And it would be translated to assembly or machine code, a horribly unsafe language. If bugs are inevitable, then no language is safe.
And which were proven to be unsecure time and time again.
>And the hypothetical alternative language would likely be implemented in C or C++.
Isn't rust self hosted now ?
>And it would be translated to assembly or machine code, a horribly unsafe language.
Not on a Lisp Machine ! ;)
But you are right. Not using C only suppress a class of vulnerabilities, not all of them (and not the most trivial to exploit ones). Unless you are prepared to go all the way and do formal verification, model checking, and NASA style development, your application will be unsecure, and it will be hacked. The question is what is (are ?) your plan to recover when it happens ?
[1] Reflections on Trusting Trust isn't relevant here, we're not considering someone trying to attack the compiler.
No, you can't. It's been shown constantly. Buffer overflows. UB. Stack overflow. Dangling pointers. Memory leaks. It will happen.
>it is literally impossible to write secure code in C (you'd have to prove it)
Nobody can prove that. But I can show that it's significantly more error prone. Most other languages have some protections against stack overflow or invalid memory access. Not to mention UB.
>my web application should be the least of anyone's worries.
Not true. Of all the parts of the stack, your webapp is likely the least tested, least used, and thus the most likely to have the most serious bugs. Not to mention that, unlike most of the software you describe, your webapp is likely handling direct user input, the most dangerous location for a bug. And let's not even get into what happens if you're storing sensitive user data.
>And the hypothetical alternative language would likely be implemented in C or C++. And it would be translated to assembly or machine code, a horribly unsafe language. If bugs are inevitable, then no language is safe.
Any backend that generates C can generate near-perfect C code. As for runtimes and compilers written in C, they'll have bugs. All programs have bugs. That's why C sucks. But for the most part, you only have to worry about three issues in the compiler: 1) a built-in function has a bug in it, and doesn't work right. This can happen in any language, including C. 2) the runtime has an error in it that allows user input to cause some kind of memory corruption bug, and exploit it. This is bad, but those codebases are heavily scrutinized, and thus are less likely to contain bugs than the hand-rolled versions that you'd write. 3) there was some kind of GC error. This is probably what you're thinking of.
Two men are walking in the woods. They come across a bear, which starts moving towards them to attack. The first man starts tying his shoes, getting ready to run. The second man asks him, "What are you doing? You can't outrun a bear." And the first man says, "I don't need to outrun the bear. I just need to outrun you."
The same is true of the GC. The GC doesn't have to be perfect, it just has to be better than you. And statistically, you suck at what the GC does.
>If bugs are inevitable, then no language is safe.
Of course no language is safe. But some are safer than others.
The same is true of the GC. The GC doesn't have to be perfect, it just has to be better than you. And statistically, you suck at what the GC does.
Aka how to sail safely Without a Paddle. ;) Like the analogy and use of statistical argument.
Using a higher level language (than C) arguably has the drawback of "hiding complexity" in exchange for better productivity and scalability. But languages like Rust and C++ (with SaferCPlusPlus) retain much of the intrinsic performance and memory efficiency of C, without introducing an additional runtime layer. That may allow you to employ extra (diverse) layers of sandboxing/jailing and other security/correctness solutions.
[1] shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus
http://www.freshports.org/devel/py-pycapsicum/
edit: people, if you have the skills please write more bindings for more languages
I don't think you're comparing apples and oranges. OpenBSD has just already done the restructuring as a separate commit; in FreeBSD you see it all at the same time, so it looks bigger. Capsicum for simple stuff is only a few lines, especially with the "helper" subroutines.
In some cases we can get more functionality with the same restriction, or more restriction than OpenBSD due to more specific constraints. So there's more code, but it also does more. It can be a trade-off.
To use the example of the unix tr utility, the change to use pledge required the standard two line diff.
if (pledge("stdio", NULL) == -1)
err(1, "pledge");
tr.c was the one of the earliest programs pledged (back when it was called tame). The original diff was a one liner before they start doing the "pledge or error out".http://marc.info/?l=openbsd-tech&m=144070638327053
+ tame(TAME_STDIO, NULL);
The capsicum diff required the followinghttps://reviews.freebsd.org/D7928 https://reviews.freebsd.org/file/data/4exxbzvuc3dayrvdj6qe/P...
+ cap_rights_t rights;
+ unsigned long cmd;
(...) + cap_rights_init(&rights, CAP_FSTAT, CAP_IOCTL, CAP_READ);
+ if (cap_rights_limit(STDIN_FILENO, &rights) < 0 && errno != ENOSYS)
+ err(1, "unable to limit rights for stdin");
+ cap_rights_init(&rights, CAP_FSTAT, CAP_IOCTL, CAP_WRITE);
+ if (cap_rights_limit(STDOUT_FILENO, &rights) < 0 && errno != ENOSYS)
+ err(1, "unable to limit rights for stdout");
+ if (cap_rights_limit(STDERR_FILENO, &rights) < 0 && errno != ENOSYS)
+ err(1, "unable to limit rights for stderr");
+
+ /* Required for isatty(3). */
+ cmd = TIOCGETA;
+ if (cap_ioctls_limit(STDIN_FILENO, &cmd, 1) < 0 && errno != ENOSYS)
+ err(1, "unable to limit ioctls for stdin");
+ if (cap_ioctls_limit(STDOUT_FILENO, &cmd, 1) < 0 && errno != ENOSYS)
+ err(1, "unable to limit ioctls for stdout");
+ if (cap_ioctls_limit(STDERR_FILENO, &cmd, 1) < 0 && errno != ENOSYS)
+ err(1, "unable to limit ioctls for stderr");
+
+ if (cap_enter() < 0 && errno != ENOSYS)
+ err(1, "unable to enter capability mode");
No one would dispute that capsicum is more capable but is significantly more complex. Pledge trades finer control over capabilities for the ability to have a "work or die" usage model. Capsicum requires that you be aware of all the potential failure cases and account for them.This is a slightly out of date example. We've since added simplifying wrappers for stdio.
The current equivalent of if pledge("stdio", ...) / err is:
if (caph_limit_stdio() < 0 || (cap_enter() < 0 && errno != ENOSYS))
err();
Some examples: https://reviews.freebsd.org/D8307> a "work or die" usage model
This is an option (or will shortly become an option) in capsicum.
>However, this is definitely something in the crosshairs of ksql(3): forking a process, like kcgi(3) does, that handles the database I/O and communicates with the master over pipes.
If they do this, then they can just use seccomp SECCOMP_SET_MODE_STRICT, which is the first thing described in the seccomp man page and in the Wikipedia page. And is also the original/first mode of seccomp to be available.
I believe capsicum would likewise be trivial to use: You can just immediately lock things down completely. (Perhaps just cap_enter() is sufficient?)
But the existing fds aren't restricted. You can use caph_limit_stream(fd, CAPH_READ/CAPH_WRITE) to restrict existing sockets down to only what is needed for stdio routines.
Capsicum may still be easy to use. And seccomp-bpf is not that complex either.
> allows resource requests or denies them and (hopefully) kills the application
Why is killing the application desirable behaviour over just denying the request?Though a sysfs/procfs config for this would go a long way.
You can combine this with the panic= kernel command-line argument to set a reboot delay, but for headed (rather than headless) boxes it may be more desirable to keep the panic message up to see what process triggered it.
The process' output can not be trusted, so it should stop processing. Detecting something you never expect to happen is almost tautologically stating the software doesn't know how to recover state.
This, of course, relies on an architecture where another process will take care of making sure an error is bubbled to the place it needs to go, pulling failsafe, etc... I will grant some environments and/or designs make this easier than others (eg. Erlang's error handling is all about letting processes die).
Only stuff that crashes is investigated.
At one end of the spectrum is the macOS sandbox, which pretty much only restricts how the process is allowed to interact with the outside world. It's not actually simplistic like the article claims; that's just the public API. If you look at the raw sandbox profiles, which Apple writes for daemons and such, it's quite fine-grained: sandbox profiles can specify which paths can be read and which can be written (with an arbitrary number of allow/deny rules, each of which can match on an arbitrary regular expression), which network ports can be opened or listened to, which preferences can be read or written, which IPC services can be accessed, and so on... there are a ton of categories. If you're on macOS, look in /usr/share/sandbox and /System/Library/Sandbox/Profiles to see what they look like. However, sandboxing only occurs when a given syscall's kernel implementation explicitly calls for it, and even the most locked-down process can access hundreds of syscalls and Mach kernel IPC calls, which based on their intended purpose should be harmless. And most of them really are harmless, but all it takes is a small oversight in one of them for an attacker to potentially corrupt memory in the kernel and gain control over the system. *
At the other end is Linux seccomp, which is designed to allow minimizing kernel attack surface. Chrome's renderer processes only get access to a tiny set of syscalls, and even the arguments to the syscalls are locked down, corresponding as closely as possible to what the process actually uses. (This is possible in part because Chrome has an unsandboxed master process that handles things like reading files for it. The renderer sandbox doesn't even allow open(2).) Thus Chrome is protected from most vulnerabilities in the Linux kernel, because there's just no way to get to the buggy code in question from within the sandbox. This isn't perfect: I've found multiple Linux kernel bugs (only one exploitable so far) that are accessible from the sandbox. And of course it doesn't make Chrome immune to privilege escalation, since attacking the kernel directly isn't the only option; IPC communication with the master process is a much larger attack surface. But that's basically unavoidable, and the situation for Chrome is still a big improvement over other operating systems. Unfortunately, as the article says, it takes quite a lot of work to adapt to new applications.
OpenBSD pledge seems somewhere in between. Unlike the other two, I don't have personal experience using it, but based on the manpage - an empty "promises" set restricts the process to _exit(2), which like seccomp should protect against kernel flaws. However, if you supply "stdio", suddenly the process gets access to 69 system calls, which is not terrible (many of the calls in the set are very basic and unlikely to contain bugs) but not as minimal as one might want. There are a bunch of random functionality lockdowns, which is good - for example, ioctl operations are locked down, "setsockopt(2) has been reduced in functionality substantially"... on the other hand, the promise categories generally seem more aligned to 'resources the program might want to access' than 'kernel API surface the program might need to use'.
Overall, definitely not bad, and I'd like to see something comparable on Linux, but I'd be more comfortable if OpenBSD also supported a more fine-grained sandbox.
* (Okay, that's a little unfair to macOS, since there are some sandbox filters that lock down kernel attack surface, such as the ioctl filter and the IOKit filters - the latter of which has been tightened over time. But that still leaves a ton of surface exposed.)
https://www.chromium.org/developers/design-documents/sandbox...:
Injected instructions are more likely to invoke their own system calls, so you are more likely to catch it if you have a more restrictive set of filters.
I have a hard time understanding how that could be true. A library either needs to use OS functionality, or it doesn't. If it needs to, managed languages cannot avoid doing so, and if it doesn't, why would non-managed languages make them, more so since a major argument for the most popular of them, C, is performance?
The only argument I can see is that managed language libraries could use (fewer) other libraries or the runtime to make such calls. Neither decreases the number of system calls made by any program.
There are other things in the works related to pledge. For example, they're going to ship a native local stub resolver that will be a functional extension of libc, permitting them to remove and simplify code from libc. That would allow sandboxing networked services without necessarily requiring any filesystem access after invoking pledge, while still permitting DNS to seamlessly work even after configuration changes. (The trick is that most client resolvers, third-party and in libc, default to 127.0.0.1:53 if /etc/resolv.conf is unavailable.)