Skip to content

Unsoundness in url_encode due to missing length check for curl_easy_escape #616

@zeroy0410

Description

@zeroy0410

Thank you for your outstanding contributions to this project!

Description

The url_encode function in curl-rust provides a safe interface for URL-encoding a byte slice &[u8]. Internally, it calls the libcurl C function curl_easy_escape.

According to the official libcurl documentation, curl_easy_escape has a strict limitation on its input size:

This function does not accept input strings longer than CURL_MAX_INPUT_LENGTH (8 MB).

The current implementation of the src/easy/handler.rs:3258, url_encode wrapper does not validate the length of the input slice before passing it to the C function. It directly casts s.len() to c_int and calls curl_easy_escape.

// In `fn url_encode`:
pub fn url_encode(&mut self, s: &[u8]) -> String {
    if s.is_empty() {
        return String::new();
    }
    unsafe {
        let p = curl_sys::curl_easy_escape(
            self.inner.handle,
            s.as_ptr() as *const _,
            s.len() as c_int, // <-- VULNERABILITY: No check if s.len() > CURL_MAX_INPUT_LENGTH
        );
        assert!(!p.is_null());
        let ret = str::from_utf8(CStr::from_ptr(p).to_bytes()).unwrap();
        let ret = String::from(ret);
        curl_sys::curl_free(p as *mut _);
        ret
    }
}

On the C side, curl_easy_escape will call curlx_dyn_init for initialization. The code is as follows:

/*
 * Init a dynbuf struct.
 */
void curlx_dyn_init(struct dynbuf *s, size_t toobig)
{
  DEBUGASSERT(s);
  DEBUGASSERT(toobig);
  DEBUGASSERT(toobig <= MAX_DYNBUF_SIZE); /* catch crazy mistakes */
  s->bufr = NULL;
  s->leng = 0;
  s->allc = 0;
  s->toobig = toobig;
#ifdef DEBUGBUILD
  s->init = DYNINIT;
#endif
}

During initialization, assert checks are only performed in debug compilation mode, while such checks are not carried out in release mode.

This omission allows a caller from safe Rust to violate the C function's contract, leading to Undefined Behavior (UB) within the C library.

Impact

An attacker can trigger this vulnerability by passing a slice larger than 8 MB to the url_encode function. This violates the precondition of curl_easy_escape and can lead to memory corruption, denial of service (crash), or other unpredictable program behavior. Because this can be triggered from safe Rust, it constitutes a soundness vulnerability in the crate.

Suggested Fix

The url_encode function must validate the input slice's length before calling the unsafe C function. If the length exceeds CURL_MAX_INPUT_LENGTH, the function should panic or return an error to prevent the unsafe call.

A possible implementation:

// Define the constant based on libcurl's documentation
const CURL_MAX_INPUT_LENGTH: usize = 8 * 1024 * 1024;

// In `fn url_encode`:
if s.len() > CURL_MAX_INPUT_LENGTH {
    // Or return an Err(..)
    panic!("Input length ({}) exceeds the maximum allowed by libcurl ({})", s.len(), CURL_MAX_INPUT_LENGTH);
}

If possible, I can initiate a PR later to fix this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions