When developers use libcurl—one of the world’s most popular HTTP(S) client libraries—they trust it to handle data safely and predictably. However, in mid-2022, a serious vulnerability was discovered that can break that trust in edge cases when reusing the same curl handle (CURL*). This post deep-dives into CVE-2022-32221, explains how you can trigger the flaw, the potential risks, and shows you mitigation tips.

How libcurl Handles Data in PUT and POST Requests

Normally, when you make HTTP PUT or POST requests with libcurl, you specify the data to send in one of two main ways:

CURLOPT_POSTFIELDS: Pass a pointer to a buffer (usually for POST) that libcurl sends.

- CURLOPT_READFUNCTION: Provide a callback that libcurl calls to get more data (popular for streaming or uploading files, usually in PUT).

If the same handle is used for both methods in succession, libcurl should switch its data source accordingly depending on which mode you configured. But with this bug, it might not.

The Vulnerability: Handle Reuse Mix-Up

If you use a curl handle for a PUT request with CURLOPT_READFUNCTION, then reuse it for a POST request with CURLOPT_POSTFIELDS, libcurl may still call your old read callback instead of using your buffer. This is only in effect when the handle is reused and the transfer type changes from PUT (with a read function) to POST (with postfields).

What Can Go Wrong?

- Unexpected Data Sent: The callback might produce stale, unintended, or incorrect data in the POST request.
- Use-After-Free: If the callback relies on memory you already freed or repurposed, it might crash or leak information.
- Application Logic Bugs: Your program might think it’s sending one set of data when it’s actually sending another.

Let’s make this more concrete. Here’s how you could trigger the bug

#include <stdio.h>
#include <string.h>
#include <curl/curl.h>

// Fake data for PUT
size_t put_read_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
    const char *data = "PUTDATA";
    size_t datalen = strlen(data);
    memcpy(buffer, data, datalen);
    return datalen;
}

int main(void) {
    CURL *curl = curl_easy_init();
    if (!curl) return 1;

    // First, do a PUT request using a read callback
    curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/put";);
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, put_read_callback);
    curl_easy_setopt(curl, CURLOPT_READDATA, NULL);
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)7);
    CURLcode res = curl_easy_perform(curl);

    // Now, reuse the same handle for a POST with fixed data (expected)
    const char *postfields = "POSTDATA";
    curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/post";);
    curl_easy_setopt(curl, CURLOPT_UPLOAD, L);             // switch off UPLOAD mode
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, postfields);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(postfields));
    /* 
       Here's where trouble starts: the read callback might still be used!
       Instead of uploading "POSTDATA", libcurl might call put_read_callback again.
    */
    res = curl_easy_perform(curl);  // This could send "PUTDATA" again (!) or crash.

    curl_easy_cleanup(curl);
    return ;
}

Output Analysis

With vulnerable libcurl versions, the second request (the POST) may send "PUTDATA" instead of "POSTDATA", or even crash if your callback accesses freed memory. This is easy to miss in busy codebases where handles are aggressively recycled.

Technical Details

The bug is in the logic libcurl uses to reset or clean up internal states for a handle that’s reused. Specifically, the flag that tells libcurl to use CURLOPT_POSTFIELDS data buffer is not properly reset if the handle previously used CURLOPT_READFUNCTION for a PUT, leading to libcurl calling the callback again for POST.

Fixed in: libcurl 7.84.

- Issue tracker: curl GitHub #8877
- Advisory: curl security advisory

Exploitability and Impact

While this is not a remote code execution bug, it does threaten both data integrity and application stability. If your read callback accesses global variables, environment info, or freed heap memory, this can go from mere data leakage to a crash or more severe exploitation.

Many server or client applications that manage a pool of curl handles for performance (such as in REST APIs, cloud uploaders, bots) are most at risk.

Don’t Mix Readfunctions and Postfields on the same handle if you must stay on an old version.

- Cleanup Handles: Use curl_easy_cleanup() and create fresh handles for significantly different requests (like changing from PUT+callback to POST+buffer).
- Test: If you’re using handle pools, add unit tests that intentionally change request types to check for data leaks.

Further Reading

- Official advisory: curl.se/docs/CVE-2022-32221.html
- NVD entry
- Issue on GitHub

Conclusion

CVE-2022-32221 shows how easy it is to make safe-looking code dangerous just by reusing an object in an unexpected way. If you’re building a tool or service that does lots of HTTP(S) work with libcurl, always stay up to date, and be careful when recycling handles—especially across fundamentally different request modes. If you want your app to send exactly what you asked for, check those APIs twice!


Feel free to ask in the comments for more details on debugging or working around this bug in your own code!

Timeline

Published on: 12/05/2022 22:15:00 UTC
Last modified on: 12/08/2022 15:29:00 UTC