FlawOpen

flawopen.com/ssrf/C-cpp

● CWE-918 · High
FlawOpen Security Research

Server-Side Request Forgery (SSRF) in C/C++ (libcurl)

Passing a user URL to libcurl with CURLOPT_FOLLOWLOCATION on lets any request make your C service call 127.0.0.1, internal services or the cloud metadata service at 169.254.169.254 (CWE-918), and a gethostbyname() check against "127." misses private, link-local and IPv6 addresses, loses to DNS rebinding and is bypassed by redirects. How a CURLOPT_OPENSOCKETFUNCTION callback checks the real address of every connection, together with a protocol allowlist, no redirects, no proxy and a capped body.

💡 Plain English Explainer (ELI5)

Imagine a switchboard that can connect calls to more than public phone lines: without a rule, a caller can reach staff-only extensions, and a forwarded call can change the destination after the first check. libcurl can likewise support schemes beyond HTTP, follow redirects, and resolve a hostname again when it opens a socket. CURLOPT_OPENSOCKETFUNCTION is the guard at the real connection point: it checks the sockaddr libcurl is about to use and returns CURL_SOCKET_BAD for a blocked address. CURLOPT_PROTOCOLS_STR limits routes to http,https; CURLOPT_FOLLOWLOCATION disables forwarding, and CURLOPT_NOPROXY prevents an environment proxy from hiding the real destination.

Core Concepts & Subsystem Terms

CURLOPT_FOLLOWLOCATION and extra protocols
Many libcurl builds support file, dict and gopher as well as HTTP, and with CURLOPT_FOLLOWLOCATION set to 1L a 302 to http://169.254.169.254/ is followed after any check on the original URL. A gopher:// URL can send arbitrary bytes to internal services such as Redis.
gethostbyname() and a string prefix
gethostbyname returns IPv4 addresses only, the check looks at the first one, and strncmp(..., "127.", 4) lets 10.0.0.1, 169.254.169.254, 0.0.0.0 and every IPv6 address through.
Check-then-fetch and DNS rebinding
The check resolves the name once and libcurl resolves it again when it connects. A DNS server with a short TTL can hand the check a public address and libcurl 127.0.0.1, so the check proves nothing about the connection.
CURLOPT_OPENSOCKETFUNCTION
libcurl calls the CURLOPT_OPENSOCKETFUNCTION callback with the exact address it is about to connect to, before creating every socket, including IP-literal hosts, reconnects and redirects. Returning CURL_SOCKET_BAD refuses the connection. Set CURLOPT_NOPROXY to "*" so an environment proxy is not the address being checked.
CURLOPT_PROTOCOLS_STR and a capped write callback
CURLOPT_PROTOCOLS_STR (libcurl 7.85.0+) set to "http,https" disables every other scheme, and a write callback that stops accepting data after a fixed number of bytes keeps a huge response from filling memory.

Framework mappings

Step-by-Step Attack Flow

Step 1

A user supplies a URL

A CGI handler or embedded web server passes a query parameter to curl_easy_setopt(curl, CURLOPT_URL, url).

Step 2

The service connects to an internal address

The URL names 127.0.0.1, a private host, 169.254.169.254, a gopher:// or file:// target, a hostname that rebinds, or a public page that redirects inward. The benign test value http://127.0.0.1:8080/health illustrates the case.

Step 3

An internal endpoint may be reachable

A server using libcurl sends requests from its host network position. Admin interfaces, HTTP cache services, or metadata endpoints may be reachable, but network policy or authentication can still deny access.

Step 4

The sample returns status only

In the shown C++ code, fetch_preview and test_webhook return only the HTTP status through CURLINFO_RESPONSE_CODE; they do not relay response bodies. This exposes limited response metadata or request side effects, while body disclosure would require additional response handling not shown.

Source Code: Flaw vs. Secure Implementation

UNPATCHED FLAW
/* fetch.c: both functions let the caller choose the destination */
#include <arpa/inet.h>
#include <curl/curl.h>
#include <netdb.h>
#include <string.h>

/* any protocol libcurl supports, and redirects are followed */
long fetch_preview(const char *url)
{
    CURL *curl = curl_easy_init();
    long status = -1;
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    if (curl_easy_perform(curl) == CURLE_OK)
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_easy_cleanup(curl);
    return status;
}

/* first IPv4 address, one string prefix; libcurl resolves again */
long test_webhook(const char *url, const char *host)
{
    struct hostent *he = gethostbyname(host);
    if (he == NULL || strncmp(inet_ntoa(*(struct in_addr *)he->h_addr_list[0]), "127.", 4) == 0)
        return -1;
    CURL *curl = curl_easy_init();
    long status = -1;
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "{\"ping\":true}");
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    if (curl_easy_perform(curl) == CURLE_OK)
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_easy_cleanup(curl);
    return status;
}
HARDENED SECURE PATCH
/* fetch.c: libcurl may only open sockets to public addresses */
#include <curl/curl.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <string.h>
#include <sys/socket.h>

struct body {
    char data[5000];
    size_t len;
};

/* reject loopback, private, link-local, CGNAT, multicast and non-global IPv6 */
static bool is_public(const struct sockaddr *sa)
{
    if (sa->sa_family == AF_INET) {
        const unsigned char *b = (const unsigned char *)&((const struct sockaddr_in *)sa)->sin_addr;
        return !(b[0] == 0 || b[0] == 10 || b[0] == 127 || b[0] >= 224
                 || (b[0] == 100 && (b[1] & 0xc0) == 64)
                 || (b[0] == 169 && b[1] == 254)
                 || (b[0] == 172 && (b[1] & 0xf0) == 16)
                 || (b[0] == 192 && b[1] == 168));
    }
    if (sa->sa_family == AF_INET6) {
        static const unsigned char mapped[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff};
        const unsigned char *b = ((const struct sockaddr_in6 *)sa)->sin6_addr.s6_addr;
        if (memcmp(b, mapped, sizeof mapped) == 0) {
            struct sockaddr_in v4 = {.sin_family = AF_INET};
            memcpy(&v4.sin_addr, b + 12, 4);
            return is_public((const struct sockaddr *)&v4);
        }
        return (b[0] & 0xe0) == 0x20;
    }
    return false;
}

/* called with the real address before every connection */
static curl_socket_t open_public(void *clientp, curlsocktype purpose, struct curl_sockaddr *address)
{
    (void)clientp;
    if (purpose != CURLSOCKTYPE_IPCXN || !is_public(&address->addr))
        return CURL_SOCKET_BAD;
    return socket(address->family, address->socktype, address->protocol);
}

static size_t keep_prefix(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    struct body *b = userdata;
    size_t n = size * nmemb;
    size_t room = sizeof b->data - b->len;
    size_t take = n < room ? n : room;
    memcpy(b->data + b->len, ptr, take);
    b->len += take;
    return take == n ? n : 0;
}

/* http and https only, no redirects, no proxy, timeouts, capped body */
long fetch_public(const char *url, const char *json_body, struct body *out)
{
    CURL *curl = curl_easy_init();
    struct curl_slist *headers = NULL;
    long status = -1;
    if (curl == NULL)
        return -1;
    out->len = 0;
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "http,https");
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
    curl_easy_setopt(curl, CURLOPT_NOPROXY, "*");
    curl_easy_setopt(curl, CURLOPT_OPENSOCKETFUNCTION, open_public);
    curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, keep_prefix);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, out);
    if (json_body != NULL) {
        headers = curl_slist_append(headers, "Content-Type: application/json");
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_body);
    }
    CURLcode rc = curl_easy_perform(curl);
    if (rc == CURLE_OK || (rc == CURLE_WRITE_ERROR && out->len == sizeof out->data))
        curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
    curl_slist_free_all(headers);
    curl_easy_cleanup(curl);
    return status;
}

/* both entry points use fetch_public */
long fetch_preview(const char *url, struct body *out)
{
    return fetch_public(url, NULL, out);
}

long test_webhook(const char *url)
{
    struct body ignored;
    return fetch_public(url, "{\"ping\":true}", &ignored);
}

Engineering & System Hardening Checklist

Sources