Securing Applications using Object Capabilities

Do you remember the famous colors.js npm package sabotage? Quoting the article on The Register:

Two popular open-source packages were recently sabotaged with mischievous
commits, creating confusion among those using the software and exacerbating
concerns about the fragility of the open-source software supply chain.

According to The Register and many security experts, there are serious “concerns about the fragility of the open-source software supply chain”.

But how comes that safe languages like Rust are affected despite being “safe” and what can we learn from languages like Savi and Pony that tackle the root cause of the problem - ambient authority.

Why Rust isn’t as secure as advertized

While being a memory-safe language, meaning that all sorts of memory-related bugs can’t happen, Rust isn’t immune against attacks like the one described in the introduction. Even worse, as Rust applications tend to have a very long list of dependencies, it becomes more and more difficult to track every single change of every single dependency. Remember, Rust is not immune against colors.js-like attacks, as Rust uses so called ambient authority. With ambient authority, any part of the whole application has the same permissions unless operating system mechanisms like OpenBSD’s tame(2), FreeBSD’s capsicum(4), Linux Security Modules (LSMS) or seccomp are employed.

The thing is, you don’t even have to call a malicious function in order to be affected. It’s enough that your application either directly or indirectly (through transitive dependencies) depend on a malicious crate (that’s how Rust’s libraries or packages are called), if that crate uses static initialization (code that runs before any other code):

extern "C" fn __malicious_init_function() {
    use std::{fs::File, io::prelude::*};
    let mut file = File::create("/tmp/malicious.txt").unwrap();
    file.write_all(b"Hello world from malicious attacker\n").unwrap();
}

#[used]
#[link_section = ".init_array"]
static __CTOR: extern "C" fn() = __malicious_init_function;

/// A public, non-malicious function
pub fn colors() { /* ... */ }

For as long as the colors() function (or any other public function of the malicious crate) is used somewhere in the dependency chain, your application would be affected as the __malicious_init_function would be called. If no function of that crate would be used, the linker would also eliminate the static initialization.

Note that a malicious init function could for example spawn a thread which could then continuously scan the whole memory for sensible data and send it out via the network.

This attack-vector puts a huge burden on the project developers to carefully review when upgrading dependencies. In my hence opinion, this is neither a proper nor is it a maintainable solution. As applications become more complex this becomes more and more relevant.

In the next section, I describe a solution that tackles the root cause of the problem, ambient authority.

Fighting ambient authority with Object capabilities in Pony and Savi

Pony and Savi are two very similar actor-based languages with a strong focus on correctness and security and of course concurrency. Whatever I say here in this section applies equally to both languages.

For example, in order to open a network connection in Savi, you have to provide a prove (an object capability) to the system that shows you are eligible to perform that operation.

Think of it as a special document that grants the holder of the document special rights, for example to enter a restricted area or perform some actions otherwise forbidden. In the real-world this could be your id card that allows you to buy alcohol in supermakets, or a VISA document granting entrance to certain countries.

Let’s look at a concrete example in Savi. The Savi prelude and the TCP library provide some structs that grant certain rights from least restrictive to most restrictive:

  • Env.Root represents the root authority allowing the holder to do anything

  • TCP.Auth grants the capability to do unlimited actions related to TCP

  • TCP.Connect.Auth grants the capability to open any number of TCP connection sockets to any remote hosts/ports.

  • TCP.Connect.Ticket grants the capability to connect to a specific host and port using the TCP protocol.

And now let’s look at how this is used in a real example:

:actor Main

  // This is our main function. It is granted all rights.
  :new (env Env)
    // obtain capability to do unlimited actions related to TCP by casting
    // the "root" authority to a more restrictive capability
    tcp_auth = TCP.Auth.new(env.root)

    // Further restrict capabilities to only allow connecting to "ntecs.de:80".
    connect_ticket = TCP.Connect.Auth.new(tcp_auth).to("ntecs.de", 80)

    // pass the capability "connect to ntecs.de:80" to the HttpClient.
    HttpClient.new(connect_ticket)

:actor HttpClient
  :let engine TCP.Engine

  :new (connect_ticket TCP.Connect.Ticket)
    // pass `connect_ticket` to TCP.Engine which connects to the host
    @engine = TCP.Engine.new(@, connect_ticket)

As you might have noticed, there is a chain of capabilities. At program start, the Main actor gets passed in the root authority, basically granting it to perform any possible operation. This root authority is then used to create a fine-grained, more restrictive capability, that only grants the permission to connect to a particular host via TCP. Whoever has a reference to that object capabililty is granted the right to do whatever capability the object represents. It’s important to understand that you can’t just create these object capabilities out of nowhere. You can only create these object capabilities if you already have access to some other object capability or root authority. For example, in order to create a TCP.Auth object capability, you need to have a reference to an Env.Root object capability. If you don’t give a function, class or actor a particular capability, and it does not receive one later on, it simply cannot perform certain operations. If you don’t pass a reference to the Stdout actor, a particular function cannot print something on stdout.

As FFI (Foreign Function Interface) can circumvent any security architecture, Pony allows to restrict FFI to only a set of “safe” packages. Savi currently misses that feature.

While a supply-chain attack in Savi or Pony could crash your application, simply by exhausting heap memory or stack, it would be impossible for them to breach security and leak sensible information to the outside simply by avoiding ambient authority.

Conclusion

Languages like Savi and Pony show that object capabilities are practical and capabale to solve many security-related problems like the mentioned colors.js npm package sabotage in the introduction, without any help of non-standard operation system feature. This is especially relevant to larger software projects with lots of dependencies, where auditing changes in the long dependency chain can be a burden. Hopefully, more languages will adopt similar principles in the future.