Rust Vulnerabilities: Most Common Issues You Need to Know

Rust is a strongly typed and safe system programming language developed by Mozilla. Over the years, it has become the language of choice to build memory-safe programs while maintaining high performance at scale. Rust is usually used for file format and protocol parsers but also on critical projects like in the new high-performance browser engine, Servo. Within the contents of this article, you'll gain access to a comprehensive guide covering the most common Rust vulnerabilities.

However, coding using memory-safe language doesn’t mean the code will be bug-free. Different kinds of rust security vulnerabilities like overflows, DoS, UaF, OOB, etc. These Can still be found and sometimes exploited to achieve remote code execution (RCE). The gist of it is that if you write code in Rust, it goes as fast as C or C++. But you will not get mysterious intermittent crashes in production or horrific security vulnerabilities, unlike in the latter two.

That is until you explicitly opt into that kind of thing. Uh oh.

Wait, what?

Rust provides safe abstractions that let you do useful stuff without having to deal with the complexities of memory layouts and other low-level arcana. But dealing with those things is necessary to run code on modern hardware, so something has to deal with it. Memory-safe languages like Python or Go typically manage this via the language runtime, and Rust follows suit.

In Rust, the standard library handles the nitty-gritty of hazardous memory accesses. It implements the basic building blocks, such as vectors, that expose a safe interface to the outside but perform potentially unsafe operations internally. To do that, they explicitly opt-in to potentially unsafe operations (read: barely reproducible crashes, security vulnerabilities) by annotating a block with unsafe, like this: unsafe { Dragons::hatch(); }.

However, Rust is different from languages like Python or Go in that it lets you use unsafe libraries outside the standard library. On the one hand, this means that you can write a library in Rust and call it from other languages, e.g., Python. Language bindings are unsafe by design. So the ability to write such code in Rust is a major advantage over other memory-safe languages such as Go. On the other hand, this opens the floodgates for the judicious use of unsafe.

The rust-lang Vulnerability

“rust-lang” is the official Rust project on GitHub, and the vulnerability we found is in the “rust-lang/rustc_codegen_gcc” repository and could allow any user to execute code in a privileged pipeline. This gave any user the ability to write code that extracts repository secrets like cloud credentials, modifies project settings, and even tampers with the source code using GitHub API. Since the privileged pipeline has access to all that.

The vulnerability was found in a workflow called “ci.yml” which is responsible for building and testing the repository’s code. Below is the vulnerable part of the workflow (you can find the full workflow here).

The-rust-lang-Vulnerability

In step “Download artifact”, the workflow downloads the “libgccjit.so” library from the “antoyo/gcc” repository. While in step “Set env” it sets the runner use it by modifying the linker environment variables.

Instant DoS Vulnerability in Rust’s Hyper Package?

Hyper is an extremely popular, low-level HTTP library written in Rust. The library doesn't function as a complete HTTP server or client; instead, it serves as a foundational "building block" for their implementation. As it contains methods for responding to requests, parsing request bodies, and generating proper HTTP responses. Currently, this is Rust’s most popular HTTP library, downloaded more than 67 million times from crates.io due to its usefulness for building more feature-rich HTTP clients and servers. Two of the most popular Rust-based HTTP clients & servers, namely reqwest and warp, are built on top of Hyper. In total, there are currently 2579 projects in crates.io that depend on Hyper.

A very common and useful function in the Hyper API is body::to_bytes, the function is used for copying a request or response body to a single Bytes buffer, for example the following insecure usage  –
pub async fn to_bytes(body: T) -> Result<Bytes, T::Error>
where
    T: HttpBody,
{
    ...
    // With more than 1 buf, we gotta flatten into a Vec first.
    let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
    let mut vec = Vec::with_capacity(cap);
    ...

The reason the above call is insecure is also detailed in the function documentation. The function does not implement any length checks. Therefore, due to the function of writing the entire body to a single buffer. The function has the capability to allocate a variable quantity of memory, directly corresponding to the size of the malicious HTTP packet.

Instant-DoS-Vulnerability-in-Rusts-Hyper

The More Dangerous Issue – Instant DoS

Some vendors might have disregarded the aforementioned warning, potentially downplaying the issue. This could be because sending a solitary HTTP request with excessively large body size, such as 64GB or any size that monopolizes all available memory, is highly impractical. The sheer volume of data involved would be immense. In practice, proxies, CDNs, WAFs, and other security measures would likely intercept such a request because of its abnormal size.

However, without any length checks, it is actually possible to abuse this issue for causing DoS even with a very small packet.

As mentioned, the to_bytes function reads chunks of data. If there is only one chunk, it just returns it. After reading the first chunk, the code checks if there is more to read. If there is nothing else waiting on the line, the code returns the first chunk read. If there is more data on the line, the code then creates a Vector with a capacity of the expected length of the body –
pub async fn to_bytes(body: T) -> Result<Bytes, T::Error>
where    T: HttpBody,
{
    ...
    // With more than 1 buf, we gotta flatten into a Vec first.
    let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
    let mut vec = Vec::with_capacity(cap);
    ...

The code populates the vector with the already-read data and awaits for the remaining data to be read.

Here, the crucial observation derives the size of the vector from the "Content-Length" header. The code directly passes the expected size to Rust's memory allocator. If the expected size is too large for the allocator, it will panic and crash the process. Since there is no limit for the “Content-Length” header’s value, it is possible to send a small request with a very large “Content-Length” value that will immediately crash the process –memory allocation of 11111111111111111111 bytes failed.

In many cases, depending on the project that uses Hyper, this could be a zero-click DoS attack. Since the scenario of an HTTP-based server receiving data from untrusted sources is extremely common.

How Can this Issue be Resolved?

Since the Hyper library does not restrict the HTTP body size by default, it is up to the developers that rely on Hyper to implement the size check in their own code, by comparing the request/response’s size_hint to some upper limit.

For example from the official documentation –

use hyper::{body::HttpBody};
let response = client.request(request).await?;
 
const MAX_ALLOWED_RESPONSE_SIZE: u64 = 1024;
 
let response_content_length = match response.body().size_hint().upper() {
    Some(v) => v,
    None => MAX_ALLOWED_RESPONSE_SIZE + 1
};
 
if response_content_length < MAX_ALLOWED_RESPONSE_SIZE {
    let body_bytes = hyper::body::to_bytes(response.into_body()).await?;
    println!("body: {:?}", body_bytes);
}

Summary

In essence, Hyper's absence of size restrictions poses a grave threat, prone to exploitation by attackers to crash HTTP systems. We highly recommend implementing a size limit on requests & responses as shown above. Offensive360 will keep notifying vulnerable Rust maintainers to address this issue, ensuring comprehensive vulnerability fixes.

Discover more from O360

Subscribe now to keep reading and get access to the full archive.

Continue reading