flawopen.com/ssrf/C-cpp
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.
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,dictandgopheras well as HTTP, and withCURLOPT_FOLLOWLOCATIONset to1La302tohttp://169.254.169.254/is followed after any check on the original URL. Agopher://URL can send arbitrary bytes to internal services such as Redis. gethostbyname() and a string prefixgethostbynamereturns IPv4 addresses only, the check looks at the first one, andstrncmp(..., "127.", 4)lets10.0.0.1,169.254.169.254,0.0.0.0and 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_OPENSOCKETFUNCTIONcallback with the exact address it is about to connect to, before creating every socket, including IP-literal hosts, reconnects and redirects. ReturningCURL_SOCKET_BADrefuses the connection. SetCURLOPT_NOPROXYto"*"so an environment proxy is not the address being checked. CURLOPT_PROTOCOLS_STR and a capped write callbackCURLOPT_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
- CWE reference for the weakness described in this guide: CWE-918
- OWASP Top 10:2025 — A01:2025 Broken Access Control
- OWASP API Security Top 10 — API7:2023 Server Side Request Forgery
- D3-OTF Outbound Traffic Filtering — This guide's outbound network restriction limits which destinations the service or host can reach.
Step-by-Step Attack Flow
A user supplies a URL
A CGI handler or embedded web server passes a query parameter to curl_easy_setopt(curl, CURLOPT_URL, url).
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.
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.
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
/* 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;
}
/* 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
- ✓Add a CI grep or Semgrep rule that flags
CURLOPT_URLset from request data and anyCURLOPT_FOLLOWLOCATION, 1Lin code that fetches user URLs. - ✓Install a
CURLOPT_OPENSOCKETFUNCTIONcallback that rejects loopback, private, link-local, CGNAT, multicast and non-global IPv6 addresses, including IPv4-mapped IPv6. - ✓Set
CURLOPT_PROTOCOLS_STRto"http,https",CURLOPT_FOLLOWLOCATIONto0LandCURLOPT_NOPROXYto"*". - ✓Set
CURLOPT_CONNECTTIMEOUTandCURLOPT_TIMEOUT, and cap the body in the write callback. - ✓Enforce IMDSv2 with a hop limit of 1 and restrict egress with a firewall as a second layer.