This is a review of a great albeit old paper (2007).
For a sufficiently complex system, mitigations are a greater benefit than trying to minimize code.
The best suggestion for sure is to provide an (internal) API that makes writing insecure code significantly harder than writing secure code. That certainly helped us in the past.
If talking browsers, then you might want to look at secure, browsing architectures that followed high-assurance, security principles that included minimizing trusted code. A few were OP1 and OP2 with OP1 inspiring Chrome’s model, Gazelle, Illinois Browser Operating System, and Quark Browser Kernel. The high-assurance, security industry just went with isolating existing browsers in VM’s on separation kernels but with middleware to host security-sensitive components in low-TCB protection domains. Nizza from CompSci illustrates what commercial players were doing better than their often BS marketing. Perseus is very similar deployed in commercial desktops and other products.
So, the best route is to do what worked far back as the 1970’s in using small, modular, layered, and highly-analyzed code for anything trusted plus minimize trust/privileges everywhere possible. That’s what above architectures do. It stopped NSA pentesters in the past. Same techniques reduce hacks today. The main difference is we have things like Rust, SPARK, TLA+, AFL, and so on to do lots of analyses without expensive experts with rare expertise. You can bet culture and maybe some market reasons, not tech, are the only thing that stopped well-funded companies from doing similarly-secure browsers given those above delivered working prototypes with small teams on non-Google/Microsoft/Mozilla budgets reusing existing components where possible with more secure isolation and integration. They just… didn’t want to do it.
A potentially still controversial claim in section 2.5: argues that systems to minimize privilege (e.g. selinux, pledge, capsicum) are just a distraction from the goal of building secure software.
Which has been disproven by (a) all the vulnerabilities they’ve contained in general and (b) the vulnerabilities expert coders missed in their systems. If expert coders were fallible (probably human too), then the average coder is going to screw up at least as much if not way more. That’s where containment, detection, and/or recovery comes in.
The Bernstein paper is a real classic — a must read, IMHO!!
Does anyone have experience putting these specific pieces of advice into practice, like eliminating trusted code through sandboxing? Is there an easier way to set up a sandboxed process than the list given in section 5.2 of the paper? Other thoughts about partitioning systems in general? And would process partitioning in a system like erlang/elixir fulfill the same security guarantees? In a memory safe language t = x[i] would have the expected, safe data flow, right?
t = x[i]
In general, any thoughts on how the contents of the paper apply to managed languages? Certainly the parsing/quoting issues are still present.
Finally, any examples of a perceived unacceptable performance hit turning out to be totally fine?
Ok, first you gotta be clear on what trusted code is. “Trusted” code, like with “Trusted Operating Systems,” means stuff that has privileges (read: power and access) to violate your security policy. These are things like your kernel, the drivers that access memory, your SSH, the stuff that configures any of that, and so on. They basically can screw up anything by default since they can access about any part of memory or the filesystem that the other privileged code is in. Go ahead and add the filesystem to the list of trusted stuff, too. ;)
So, your goal is:
(a) Splitting the system between that trusted code and untrusted code that just operates in its own space on its own data. You put as much functionality as you can in user-mode, capability-secure mode, strict SELinux policies, memory-safe languages, whatever. Any way of reducing access or damage it has. So, that’s minimizing trusted by making most of it untrusted (or less trusted).
(b) Then, like in a distributed system, you make them talk with message passing of some form where an untrusted function asks a trusted function to perform an operation, the trusted function checks everything about that for sanity/safety/security, only performs the operation if it’s allowed, and does it carefully since it could be tricked. This is why we say the trusted code should use the simplest constructs and control flow possible with plenty checks. Paranoid, coding style is the norm here due to the high risk that trusted code carries.
A few, quick examples. You can look at browsers like OP/OP2 or IBOS I posted above compared to how browsers are normally done monolithically for one. In VAX VMM time, they’d often deal with shell’s complexity by having a dead-simple shell for the security kernel in privileged mode that complex commands got compiled into with option of visual inspection before running them. For graphics, they might use a virtual screen that’s run through a trusted renderer/output with each process or VM having its own virtual screen its X windows writes to, no access to each others’ keyboard-mouse-screen system, and a trusted subsystem moves data to/from whichever the kernel says is supposed to have focus. Nitpicker GUI is like that.
Far as managed languages, what do you mean by that is first question? If just memory-safe, then it will knock out lots of problems. Some are designed to reduce memory leaks, too. You generally have to look at each kind of vulnerability, look to see if language properties deal with it, and go from there. Now, many “managed” languages have huge runtimes and standard libraries written in unsafe languages like C: Java, .NET, PHP, and so on. Depending on design, they might also run with more privileges than a normal, native app due to default install being setup to run all kinds of applications with ease of use for developers. I don’t trust any of them for securely sandboxing things since their complexity, unsafety, and higher-than-average privilege has led hackers to target the crap out of them. In theory, a safer runtime can be done like the safety-critical runtimes for Java in embedded sector or the native-focused compilers for C# to do stuff like VerveOS. It could be done but is not done. So, I say go with one close to the metal that just makes common operations safe-by-default.
Far as performance hit, look for something similar to what you’d create in a language like Java, C#, Go, Python, and so on. If any of those are acceptable, then something like Ada, SafeD, Rust, or Nim should be able to handle it. Rust is unique where it can often, but not always, maintain high performance with its safety scheme. Hell, MirageOS is doing a unikernel and TLS in Ocaml for goodness sakes. Hardware is so fast it can cover a lot of the overhead for safe stuff these days. Then, worst case, you can always profile the safe code to identify areas where you can selectively disable safety features in fast-path after maybe using static analysis or something like SPARK to show a specific check isn’t needed. There’s lots of work in compiler research and program analysis to try to eliminate unnecessary checks. Some of the stuff for legacy C not designed for verification or safety has knocked penalties down from 3-10x slower (!!!) to 1.4-2x slower. Lots of progress.