Software Engineering

What Recent Vulnerabilities Mean to Rust


In recent weeks several vulnerabilities have rocked the Rust community, causing many to question the safety of the borrow checker, or of Rust in general. In this post, we will examine two such vulnerabilities: The first is CVE-2024-3094, which involves some malicious files in the xz library, and the second is CVE-2024-24576, which involves command-injection vulnerabilities in Windows. How did these vulnerabilities arise, how were they discovered, and how do they involve Rust? More importantly, might Rust be susceptible to more similar vulnerabilities in the future?

Last year we published two blog posts about the security provided by the Rust programming language. We discussed the memory safety and concurrency safety provided by Rust’s borrow checker. We also described some of the limitations of Rust’s security model, such as its limited ability to prevent various injection attacks, and the unsafe keyword, which allows developers to bypass Rust’s security model when necessary. Back then, our conclusion was that no language could be fully secure, yet the borrow checker did provide memory and concurrency safety when not bypassed with the unsafe keyword. We also examined Rust through the lens of source and binary analysis, gauged its stability and maturity, and realized that the constraints and expectations for language maturity have slowly evolved over the decades. Rust is moving in the direction of maturity today, which is distinct from what was considered a mature programming language in 1980. Furthermore, Rust has made some notable stability guarantees, such as promising to deprecate rather than delete any crates in crates.io to avoid repeating the Leftpad fiasco.

CVE-2024-3094 is fascinating from an origin standpoint. The source of the vulnerability in the CVE has nothing to do with Rust, because xz is written in C. It is arguably a backdoor rather than a vulnerability, implying malicious intent rather than simple human error by the developers. The CVE was published on March 29, and it affects the newest versions (5.6.0 and 5.6.1) of xz, but not 5.4.6 or any older versions. Many articles and posts have discussed this vulnerability so, for this post, we shall focus on its impact on Rust.

On September 23, 2023, the first version (0.1.20) of the crate liblzma-sys was published on crates.io. This crate is a low-level Rust wrapper around the xz C code. Since then, there have been 14 newer versions of the crate published, with more than 25,000 downloads, and two separate crates that depend on it. The first vulnerable instance of the liblzma-sys crate was published on April 5. However, on April 9, Phylum reported that the xz backdoor existed in several of the latest versions of this crate. As of this writing, liblzma-sys’s latest version is 0.3.3, and versions 0.3.0 through 0.3.2 have been yanked. That is, these versions are still available from crates.io, but not for direct download; only for any other Rust crates that downloaded them before yanking. (This demonstrates crates.io’s compliance with the principle that old, even insecure crates are never deleted; they are merely deprecated). Consequently, the vulnerability has been “patched” for Rust.

What does this vulnerability reveal about Rust? The vulnerability was a backdoor to a non-Rust project; consequently, it reveals nothing about the language security of Rust itself. From a Rust perspective, this was a supply-chain vulnerability related to library reuse and interface wrapping. The crates.io service had been importing the liblzma-sys crate for 6 months with no problems. The challenge of software supply chain risk management and software composition and reuse is significant and affects all complex software. It is disturbing that for 1 week, the backdoor was known in the C community, but not the Rust community. However, within 24 hours of being made aware, the crates.io maintainers were able to patch the crate. We can also credit Phylum’s monitoring service, which detected the vulnerability migrating from C to Rust.

“BatBadBut” Command Injection with Windows’ cmd.exe (CVE-2024-24576)

Like CVE-2024-3094, CVE-2024-24576 first appeared outside of Rust but can apply to many languages including Rust. To understand this vulnerability, we must first dig into history and basic cybersecurity.

The vulnerability is an example of OS command injection (CWE-79). There are many other pages, such as SEI CERT Secure Coding rule IDS07-J (for Java) that provide a gentle introduction and explanation of this CWE. As the CERT rule suggests, Java provides APIs that sanitize command-line arguments with the only catch being that you must provide the command and arguments as a list of strings rather than as one long string. Most other languages, including Rust, provide similar APIs, with the oldest example being the C exec(3) function family, standardized in POSIX. These replace older functions such as the standard C system() function, which took a command as a single string and was thus vulnerable to command injection. In fact SEI CERT Secure Coding rule ENV33-C goes so far as to deprecate system().

The shells associated with Linux, such as Bash and the C shell, are consistent about quoting. They tokenize arguments and provide any invoked programs with an argument list rather than the original command string. However, Windows’ cmd.exe program, used for executing Windows .bat (batch) files, tokenizes arguments differently, which means the standard algorithms for sanitizing untrusted arguments are ineffective when passed to a batch program on Windows.

This problem has been reported for more than a decade, but was most widely publicized by RyotaK on April 9. Called the BatBadBut vulnerability, it was consequently published by the CERT Coordination Center and affected several languages. Many of these languages subsequently had to release security patches or update their documentation. Interestingly, of the top 10 Google hits on the search term “BatBadBut,” 5 of them are specific to Rust. That is, they mention that Rust is vulnerable without including the fact that several other languages are also vulnerable.

On a related note, Java was an unusual case. Oracle has declared that they will neither modify Java nor update its documentation. It is likely that Oracle already addressed this problem in Java SE 7u21. They adjusted Java’s internal tokenization of Runtime.exec() to accommodate cmd.exe (on Java for Windows). In Java SE 7u25, they added a property jdk.lang.Process.allowAmbigousCommands to resurrect the original behavior in limited circumstances. (There were 80 updates of Java SE7 and 401 updates of Java SE8, so Oracle was very busy securing Java at the time.)

Turning back to Rust, it had naïve command-line sanitization and was thus vulnerable to OS command injection when run on Windows, while documenting that it sanitized arguments to prevent command injection. This affected all versions of Rust before 1.77.2.

What does this vulnerability reveal about Rust? Rust’s command sanitization routines had appeared to be adequate; they are sufficient for Linux programs. Rust was vulnerable to a weakness that also affected many other languages including Haskell, PHP, and Node.js. To prevent this vulnerability from affecting Rust before April 9, the Rust developers would have had to discover the vulnerability themselves. Finally, we can also credit RyotaK for reporting the vulnerability to the CERT/CC.

Rust Software Security Versus the Real World

In the context of Rust software security, what have we learned from these two issues? Neither of these issues specifically target Rust, but Rust programs are affected nonetheless. Rust’s borrow checker makes Rust just as secure as it ever was for memory safety and concurrency. The borrow checker’s memory and concurrency safety and security do have limitations, and the borrow checker also does not protect against the types of interface and dependency vulnerabilities that we discuss here. Both issues indicate weaknesses in platforms and libraries, and only affect Rust after Rust tries to support these platforms and libraries.

The military often says that no good battle plan survives contact with the enemy. I would apply this proverb to Rust in that the security of no programming language survives contact with the real world. That is why having stability and maturity in a language is important. Languages need to be updated, but developers need a predictable path. Integrating any language with the real world forces vulnerabilities and weaknesses onto the language, and some of these vulnerabilities can remain dormant for decades, often surfacing far from the Rust community.

Like the Java and PHP communities, the Rust community must make Rust interface with the wider computing world and the Rust community will make some mistakes in doing so. The Rust community will have to assist in discovering these vulnerabilities and mitigating them both in Rust and in the platforms and libraries from where they originate. As for Rust developers, they must, as usual, remain vigilant with applying updates to the Rust tools they use. They should also avoid crates that are deprecated or yanked. And they should also be aware of supply chain issues that may enter the Rust world via crates to external libraries.