So close to getting to the actual root issue (ambient authority), but so far away (blaming the programmer, instead of structural deficits in the OS).
We need operating systems that don't hand out authority like candy if we're ever going to have the usability and freedom that we used to have in the 1980s with floppy based IBM PCs running MS-DOS.
I'm constantly dismayed at the ongoing failure of imagination that accepts a security model designed for a computer shared by co-workers in a small department in a corporation, as the model to chose in the age of mobile code and persistent networking.
It's as if we've designed our infrastructure out of crates of Dynamite and wonder why things keep blowing up.
I agree that ambient authority is the root issue here, but not necessarily at the OS level.
Since logging is just another bit of code intertwined with your other code, it's not clear to me that OS capabilities would help here.
Let's say you have an app that needs JNDI and also needs logging, So JNDI is given a network capability, then log4j calls it. Now we just have a confused deputy within your code.
Ah - but log4j shouldn't be able to get JNDI without being given it! Or the Network object. Or whatever is the thing you need to get a job done. This is the argument by reachability of capability security.
Now you need to ensure that your internal code is structured along those lines, which is really what the OP is getting at.
> So close to getting to the actual root issue (ambient authority), but so far away (blaming the programmer, instead of structural deficits in the OS).
Indeed, this is the entire point of the post! (Well, the second point of the post. The "deeper issue".)
- The root cause is ambient authority: the fact that log4j (and its further dependencies like JNDI) can just create a network connection without having been explicitly given the ability to do so.
- This is not the fault of the programmer. I was focusing blame on the programming language not having capabilities, though as many commenters including you have pointed out, capabilities at the OS level are also very important. Ultimately you want both, and they complement each other.
> the fact that log4j (and its further dependencies like JNDI) can just create a network connection without having been explicitly given the ability to do so.
i dont think this is the responsibility of log4j to express it's capabilities like that. The responsibility should be in the runtime - like the JVM, or even the OS.
Services should run in a sandbox, like how a browser runs a webpage in a sandbox, and only allow capabilities explicitly being requested, and require a human to allow it. Or, have a sandbox such that no matter what code executes, it cannot do more harm than a pre-determined amount (such as limited disk space, limited CPU usage, network usage etc).
The root cause is the security model of modern applications.
We need to go even further for apps installed in mobile devices: We need to make it impossible for apps to determine that they've been denied a capability. Many useful apps refuse to run at all unless you give them access to personal information (e.g. your location) when there's no need for them to have such information to function. The solution is for the OS to spoof that capability by providing hostile apps with random data, or to otherwise fail to provide accurate data to the apps.
Of course a more ideal solution is for app stores to refuse to allow apps that list capabilities they cannot justify a need for, but at least in the Android world this doesn't seem to be happening.
Operating systems are also written by programmers. An operating system written in a capability-safe language would not have such ambient authority problems.
Please let me re-state my objection to your assertion, in a different, and hopefully more constructive, manner.
An Operating System is a program that multiplexes hardware resources and makes them safely usable by a number of applications. If the design of that system is flawed, how could a capability-safe language do anything to correct the problem?
It is my assertion that the design of Linux, Windows, MacOS are all flawed. They provide, by default, any program the computer equivalent of "power of attorney". All you have to do is confuse, or subvert any program the user runs one time to abuse that privilege on behalf of an outside influence, and the system is on the way to being pwned.
Memory safe, and even capability safe languages won't do any good, if the underlying OS doesn't enforce the will of the user, but instead freely gives the users authority to any program they happen to run, intentional or otherwise.
I'm not saying memory or capability safety isn't useful, I'm just saying it isn't sufficient.
If that were true, you could cross compile Windows 10 to Rust Source code (or whatever language you think its magic enough to be safe), compile it from that language, and then it would never have a security problem again.
Except the vulnerability was a stack up. The logger wasn't making network calls per se, but passing requests to a component (JNDI) that would obviously need network access. You'd have the same root issue, it'd just manifest more as sort of a confused deputy with a capability based model.
Practically (and like is being suggested here in other comments), JNDI would probably be a separate component (maybe a separate process) with network access, and log4j would just send those user provided strings over an IPC channel, and you'd be in exactly the same place at the end of the day.
A fun excerpt from "Capabilities: Effects for Free"
Capability-safe languages prohibit the ambient authority [18] that is present in noncapability-safe languages. An implementation of a logger in Java, for example, does
not need to be initialised with a log file capability, as it can simply import the appropriate file-access library and open the log file for appending by itself. But critically, a
malicious implementation could also delete the log, read from another file, or exfiltrate
logging information over the network. Other mechanisms such as sandboxing can be
used to limit the damage of such malicious components, but recent work has found that
Java’s sandbox (for instance) is difficult to use and therefore often misused [2, 12].
What you need is not a capability safe language, but rather a tool to make application jails and similar systems more accessible.
Your os exists for a reason, let it handle the sandboxing for you, and not the language.
Otherwise you fall into troubles later with different sandboxing vulnerabilities in different compilers.
Have a codebase that compiles using deprecated features which are removed on language version 2.1 ?
“Tough luck, security patches only exists in 3.1 and above, guess you’ll have to stay unsafe”
Not to mention the pain it would bring on languages to backport safety features to older LTS releases of the compiler.
Just why?, let the os handle these things for you. It is called an operating system for a reason.
Log4j vulnerability was minimised in any organisation that restricts its applications network capability to whitelisted machines only, with the help of its OS features.
(Yes your application might need network access, so run it through a reverse proxy and filter the traffic based on strict set of rules)
Capabilities model is not about "features" which might be deprecated, but more about "access to standard resources". Capabilities e.g. doesn't care that you are running version 3.1.73 of a certain dependency, it cares that you want to open a network connection to 123.76.34 or that you want /usr/bin/secret file.txt.
The version [of a dependency] starts to matter if it blocks upgrade to a version of the platform that would allow to control a specific new capability.
This is a good point. I talked about capabilities at the language level, but they're just as important if not more-so at the operating system level.
If you have capabilities in the programming language, then you can make guarantees about what resources (like the network) your program's transitive dependencies can use (in process).
If you have capabilities in the OS, then you can make guarantees about what resources a process can use. EDIT: this is how you would use capabilities in the OS to obtain "application jails". But capabilities would be more flexible here, for example by allowing processes to share them.
As monocasa points out in a separate thread, log4j is actually a messy example---messier than I realized when I wrote this post---because the way it (is? might be?) set up is that your application runs in one process, and JNDI in another. Which brings up a lot of design questions around capabilities in the OS and in the language and in how they interact that I don't know much about.
But the primary point of capabilities is to limit how access control can flow from place to place, using standard data-flow mechanisms. In a programming language, this means tying access to whether you have a pointer to an object. In an OS, I think this looks more like OS-level permissions granted to a process, that processes can share with each other.
You don't have some big table somewhere saying who is allowed to do what. Instead, access is granted by a thing, that you can pass around. When you get used to the idea, it just feels very natural and powerful.
So, IDK what the Java world had that was similar, but in .NET at one time they tried to solve this at the runtime VM level with the concept of Security Permissions and trust levels. In practice, this wound up being:
- Somewhere between too confusing/frustrating to developers (especially ASP.NET ones, where often the escape hatch of 'AllowPartiallyTrustedCallersAttribute' would get thrown around)
- Hard to manage from a per-app granularity standpoint.
Edit, hit the post button too early:
In any case, this behavior wound up getting changed to be a lot more forgiving in .NET 4 (although, often in fact requiring the removal of the aforementioned APTCA from your web projects,) and IIRC it's gone in Core.
There's what I think is a similar concept in Java called security contexts. I've never encountered it actually being used, and it just tends to get in the way, like you said. These strike me as similar to SELinux in that regard.
Capabilities are underrated as a generally way to purge bad archictures, make it clearer what code is doing, and generally cut accidental complexity & improve programmer productivity.
This is a big deal, because many security practices are neutral or bad for programmer productivity.
We need a big project to get CloudABI implemented in all the major kernels to make the theory reality. Whereas before it was unclear what was a good candidate to get this stuff in prod, now it is very clear that socket-activated services are an idea use-case, with very little migration pain.
Even if you think we should be going to Fuschia or SEl4 or whatever, I think this is a good stepping stone. Those projects are a big jump alone, and funding is uncertain. (Plus there are issues of single-company dominance with Fuchsia.) I think CloudABI is the sort of "non-reformist reform" not "worse is better" stepping stone that would help those projects not hurt them.
Agreed re: the general idea, but isn't CloudABI in particular superseded somewhat by WASI? Its repo seems to say it is: https://github.com/NuxiNL/cloudabi
(WASI is similarly capability-based, as I understand it!)
I don't think so. WASM is changing things on many fronts. CloudABI is just doing one front.
I don't have any thing against WASI, and I don't blame them for wanting to point out a like-minded project that was still active. But just as I think CloudABI is a good stepping stone for seL4 or Fuschia, I think it is a good stepping stone for WASM.
Also, I guess I don't believe in coupling change on in principle independent axes. If you at least allow the knobs to be turned separately, even if you don't e.g. CI or otherwise support all combinations, you are incentivized to handle things more "parametrically" vs if-def soup (which matches capabilities, incidentally!) and you have a great way to troubleshoot stuff. This is like how NetBSD says they like supporting obscure architectures to catch more bugs in the portable code too, not just make their lives harder.
Making an embryonic process, mutating it's state as desired, and then submitting to the scheduler is a much nicer workflow, and more in the spirit of capabilities anyways where "fork = duplicate the whole keyring and then destroy some caps" as foolhardy.
FreeBSD already had process/PID FDs, but I think CloudABI avoided them because it wanted to be easier to port. But now that Linux has them too, I don't think this should be a such a portability concern.
4. Bang on drum for other OSes and upstream systemd to implement this stuff we can can good portable abstractions -- I think this is our best shot to get "portable containers".
Java has actually had this built in for a long time now. A SecurityManager allows you to restrict access to things like the filesystem and the network (and whatever else you want). I have never seen it used in a real codebase.
Elasticsearch is actually using SecurityManager with quite thoroughly locked down policies; and it seems that this actually saved ES from being vulnerable to the RCE.
The irony is now that OpenJDK just recently decided to deprecate the SecurityManager in Java 17 and remove it in Java 18.
We actually use it in a real codebase. When the software is running on "developer mode" (on a developer's machine), we install a SecurityManager which denies all outbound connections except for localhost by default.
The security manager represents a flawed way to do it. It tries to catch up and restrict an application after it already has access to the releveant APIs. Forget to restrict just one API, and the sandbox can be escaped. Usually, the integration with the security manager requires intimate knowledge of the application.
Javascript follows a better approach because it has a small core API. All other APIs have to be explicitly injected and permitted by the host program.
Java's SecurityManager is very different from capability-safety. It's much more like setuid: SecurityManager allows or disallows network accesses depending on what code is running. (Literally by inspecting the stack)
We need operating systems that don't hand out authority like candy if we're ever going to have the usability and freedom that we used to have in the 1980s with floppy based IBM PCs running MS-DOS.
I'm constantly dismayed at the ongoing failure of imagination that accepts a security model designed for a computer shared by co-workers in a small department in a corporation, as the model to chose in the age of mobile code and persistent networking.
It's as if we've designed our infrastructure out of crates of Dynamite and wonder why things keep blowing up.
Since logging is just another bit of code intertwined with your other code, it's not clear to me that OS capabilities would help here.
Let's say you have an app that needs JNDI and also needs logging, So JNDI is given a network capability, then log4j calls it. Now we just have a confused deputy within your code.
Ah - but log4j shouldn't be able to get JNDI without being given it! Or the Network object. Or whatever is the thing you need to get a job done. This is the argument by reachability of capability security.
Now you need to ensure that your internal code is structured along those lines, which is really what the OP is getting at.
Indeed, this is the entire point of the post! (Well, the second point of the post. The "deeper issue".)
- The root cause is ambient authority: the fact that log4j (and its further dependencies like JNDI) can just create a network connection without having been explicitly given the ability to do so.
- This is not the fault of the programmer. I was focusing blame on the programming language not having capabilities, though as many commenters including you have pointed out, capabilities at the OS level are also very important. Ultimately you want both, and they complement each other.
i dont think this is the responsibility of log4j to express it's capabilities like that. The responsibility should be in the runtime - like the JVM, or even the OS.
Services should run in a sandbox, like how a browser runs a webpage in a sandbox, and only allow capabilities explicitly being requested, and require a human to allow it. Or, have a sandbox such that no matter what code executes, it cannot do more harm than a pre-determined amount (such as limited disk space, limited CPU usage, network usage etc).
The root cause is the security model of modern applications.
Of course a more ideal solution is for app stores to refuse to allow apps that list capabilities they cannot justify a need for, but at least in the Android world this doesn't seem to be happening.
Deleted Comment
An Operating System is a program that multiplexes hardware resources and makes them safely usable by a number of applications. If the design of that system is flawed, how could a capability-safe language do anything to correct the problem?
It is my assertion that the design of Linux, Windows, MacOS are all flawed. They provide, by default, any program the computer equivalent of "power of attorney". All you have to do is confuse, or subvert any program the user runs one time to abuse that privilege on behalf of an outside influence, and the system is on the way to being pwned.
Memory safe, and even capability safe languages won't do any good, if the underlying OS doesn't enforce the will of the user, but instead freely gives the users authority to any program they happen to run, intentional or otherwise.
I'm not saying memory or capability safety isn't useful, I'm just saying it isn't sufficient.
Obviously that's not true.
Deleted Comment
Deleted Comment
Capability-safe languages prohibit the ambient authority [18] that is present in noncapability-safe languages. An implementation of a logger in Java, for example, does not need to be initialised with a log file capability, as it can simply import the appropriate file-access library and open the log file for appending by itself. But critically, a malicious implementation could also delete the log, read from another file, or exfiltrate logging information over the network. Other mechanisms such as sandboxing can be used to limit the damage of such malicious components, but recent work has found that Java’s sandbox (for instance) is difficult to use and therefore often misused [2, 12].
Emphasis added my own.
[1] https://www.cs.cmu.edu/~aldrich/papers/effects-icfem2018.pdf
Your os exists for a reason, let it handle the sandboxing for you, and not the language.
Otherwise you fall into troubles later with different sandboxing vulnerabilities in different compilers.
Have a codebase that compiles using deprecated features which are removed on language version 2.1 ?
“Tough luck, security patches only exists in 3.1 and above, guess you’ll have to stay unsafe”
Not to mention the pain it would bring on languages to backport safety features to older LTS releases of the compiler.
Just why?, let the os handle these things for you. It is called an operating system for a reason.
Log4j vulnerability was minimised in any organisation that restricts its applications network capability to whitelisted machines only, with the help of its OS features.
(Yes your application might need network access, so run it through a reverse proxy and filter the traffic based on strict set of rules)
If you have capabilities in the programming language, then you can make guarantees about what resources (like the network) your program's transitive dependencies can use (in process).
If you have capabilities in the OS, then you can make guarantees about what resources a process can use. EDIT: this is how you would use capabilities in the OS to obtain "application jails". But capabilities would be more flexible here, for example by allowing processes to share them.
As monocasa points out in a separate thread, log4j is actually a messy example---messier than I realized when I wrote this post---because the way it (is? might be?) set up is that your application runs in one process, and JNDI in another. Which brings up a lot of design questions around capabilities in the OS and in the language and in how they interact that I don't know much about.
But the primary point of capabilities is to limit how access control can flow from place to place, using standard data-flow mechanisms. In a programming language, this means tying access to whether you have a pointer to an object. In an OS, I think this looks more like OS-level permissions granted to a process, that processes can share with each other.
You don't have some big table somewhere saying who is allowed to do what. Instead, access is granted by a thing, that you can pass around. When you get used to the idea, it just feels very natural and powerful.
- Somewhere between too confusing/frustrating to developers (especially ASP.NET ones, where often the escape hatch of 'AllowPartiallyTrustedCallersAttribute' would get thrown around)
- Hard to manage from a per-app granularity standpoint.
Edit, hit the post button too early:
In any case, this behavior wound up getting changed to be a lot more forgiving in .NET 4 (although, often in fact requiring the removal of the aforementioned APTCA from your web projects,) and IIRC it's gone in Core.
This is a big deal, because many security practices are neutral or bad for programmer productivity.
We need a big project to get CloudABI implemented in all the major kernels to make the theory reality. Whereas before it was unclear what was a good candidate to get this stuff in prod, now it is very clear that socket-activated services are an idea use-case, with very little migration pain.
Even if you think we should be going to Fuschia or SEl4 or whatever, I think this is a good stepping stone. Those projects are a big jump alone, and funding is uncertain. (Plus there are issues of single-company dominance with Fuchsia.) I think CloudABI is the sort of "non-reformist reform" not "worse is better" stepping stone that would help those projects not hurt them.
(WASI is similarly capability-based, as I understand it!)
I don't have any thing against WASI, and I don't blame them for wanting to point out a like-minded project that was still active. But just as I think CloudABI is a good stepping stone for seL4 or Fuschia, I think it is a good stepping stone for WASM.
Also, I guess I don't believe in coupling change on in principle independent axes. If you at least allow the knobs to be turned separately, even if you don't e.g. CI or otherwise support all combinations, you are incentivized to handle things more "parametrically" vs if-def soup (which matches capabilities, incidentally!) and you have a great way to troubleshoot stuff. This is like how NetBSD says they like supporting obscure architectures to catch more bugs in the portable code too, not just make their lives harder.
On its Wikipedia entry, so most likely it won't go anywhere.
Making an embryonic process, mutating it's state as desired, and then submitting to the scheduler is a much nicer workflow, and more in the spirit of capabilities anyways where "fork = duplicate the whole keyring and then destroy some caps" as foolhardy.
FreeBSD already had process/PID FDs, but I think CloudABI avoided them because it wanted to be easier to port. But now that Linux has them too, I don't think this should be a such a portability concern.
1. Work on FreeBSD cross in Nixpkgs, because I need a way to pin forks and run nice VM tests without going insane. (We already have NetBSD cross.)
2. Rig up a booting image that uses https://github.com/InitWare/InitWare, the fork of systemd.
3. Add support to CloudABI in initware.
4. Bang on drum for other OSes and upstream systemd to implement this stuff we can can good portable abstractions -- I think this is our best shot to get "portable containers".
https://docs.oracle.com/javase/tutorial/essential/environmen...
The irony is now that OpenJDK just recently decided to deprecate the SecurityManager in Java 17 and remove it in Java 18.
See also this Twitter thread: https://twitter.com/rcmuir/status/1469730949810339843
Javascript follows a better approach because it has a small core API. All other APIs have to be explicitly injected and permitted by the host program.