Memory Leak in libcurl via Location Header Handling (CWE-770)
What the report claims
Memory leak in libcurl when processing HTTP 3xx redirect responses with a Location header, causing gradual memory consumption increase and potential denial of service due to resource exhaustion.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
Why this finding?
- Curl_req_hard_reset and Curl_req_free both set req->location = NULL without freeing the prior allocation — confirmed in both master and release curl-8_13_0.
- Curl_copy_header_value allocates via malloc (via Curl_memdup0) — confirmed.
- The allocation site (http_header function parsing Location: header into req->location) is not in the provided code snippets, so the end-to-end leak path cannot be fully verified from the given evidence.
- If the allocation occurs as described, this would be a repeated leak on each redirect (large leak), which the project policy treats as a potential security issue (DoS risk).
What still needs checking
- The implementation of http_header() in lib/http.c (where Location header is parsed and stored to req->location).
How this analysis ran 5 claims · 8 citations verified
Checked against curl master on 2025-05-22 (203b4349af3e). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Review a draft reply to the reporter
Edit and verify before sending.
Thank you for the detailed report. The provided code confirms that Curl_req_hard_reset and Curl_req_free do not free req->location, and that Curl_copy_header_value allocates via malloc. However, the http_header() function implementation (where the Location header would be parsed and stored into req->location) was not included in the source snippets we reviewed. Could you point to the exact lines in http_header that assign to req->location, or provide that code section? This will let us confirm the full allocation→leak path.
Inspect all claims and source code 5 claims · 9 code excerpts
Report claims
Memory allocated for the Location header's value is not properly deallocated when the Curl_easy handle is reused for subsequent requests
Read the reasoning
The provided code confirms that Curl_req_hard_reset and Curl_req_free do not free req->location, and that Curl_copy_header_value allocates memory via malloc. However, the implementation of http_header() where the Location header is allegedly parsed and stored into req->location is not included in the provided source snippets (only its declaration at lib/http.c:106 is shown). Without seeing the allocation site, the full claim cannot be verified from the provided code alone.
The leak occurs in the handling of HTTP 3xx redirect responses containing a Location header
Read the reasoning
The claim describes the leak occurring in HTTP 3xx redirect handling with Location headers. The http_header function implementation (where header parsing occurs) is not in the provided code, so the trigger location cannot be verified.
A malicious server can trigger the leak by sending HTTP 3xx responses with long Location headers that cause repeated allocation without deallocation
Read the reasoning
The trigger claim depends on http_header() allocating and storing the Location header value into req->location. Since http_header's implementation is not provided, this attack path cannot be verified from the given code.
The impact is gradual memory consumption increase leading to potential denial of service due to resource exhaustion
Read the reasoning
Impact claim depends on C1-C3 being verified. Since the allocation path is not confirmed in the provided code, the DoS impact cannot be assessed from the provided evidence alone.
The reproduction involves using Valgrind to monitor memory allocations while following redirects with a malicious server that provides long Location headers
Read the reasoning
This describes reproduction steps, not a code claim. Valgrind output and server logs are not provided in the evidence.
Read the original report (excerpt)
Source code
| 75 | if(result) |
| 76 | return result; |
| 77 | |
| 78 | if(!req->sendbuf_init) { |
| 79 | Curl_bufq_init2(&req->sendbuf, data->set.upload_buffer_size, 1, |
| 80 | BUFQ_OPT_SOFT_LIMIT); |
| 81 | req->sendbuf_init = TRUE; |
| 82 | } |
| 83 | else { |
| 84 | Curl_bufq_reset(&req->sendbuf); |
| 85 | if(data->set.upload_buffer_size != req->sendbuf.chunk_size) { |
| 86 | Curl_bufq_free(&req->sendbuf); |
| 87 | Curl_bufq_init2(&req->sendbuf, data->set.upload_buffer_size, 1, |
| 88 | BUFQ_OPT_SOFT_LIMIT); |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | return CURLE_OK; |
| 93 | } |
| 94 | |
| 95 | CURLcode Curl_req_start(struct SingleRequest *req, |
| 96 | struct Curl_easy *data) |
| 97 | { |
| 98 | req->start = curlx_now(); |
| 99 | return Curl_req_soft_reset(req, data); |
| 100 | } |
| 101 | |
| 102 | static CURLcode req_flush(struct Curl_easy *data); |
| 103 | |
| 104 | CURLcode Curl_req_done(struct SingleRequest *req, |
| 105 | struct Curl_easy *data, bool aborted) |
| 106 | { |
| 107 | (void)req; |
| 108 | if(!aborted) |
| 109 | (void)req_flush(data); |
| 110 | Curl_client_reset(data); |
| 111 | #ifndef CURL_DISABLE_DOH |
| 112 | Curl_doh_close(data); |
| 113 | #endif |
| 114 | return CURLE_OK; |
| 115 | } |
| 116 | |
| 117 | void Curl_req_hard_reset(struct SingleRequest *req, struct Curl_easy *data) |
| 118 | { |
| 119 | struct curltime t0 = {0, 0}; |
| 120 | |
| 121 | Curl_safefree(req->newurl); |
| 122 | Curl_client_reset(data); |
| 123 | if(req->sendbuf_init) |
| 124 | Curl_bufq_reset(&req->sendbuf); |
| 125 | |
| 126 | #ifndef CURL_DISABLE_DOH |
| 127 | Curl_doh_close(data); |
| 128 | #endif |
| 129 | /* Can no longer memset() this struct as we need to keep some state */ |
| 130 | req->size = -1; |
| 131 | req->maxdownload = -1; |
| 132 | req->bytecount = 0; |
| 133 | req->writebytecount = 0; |
| 134 | req->start = t0; |
| 135 | req->headerbytecount = 0; |
| 136 | req->allheadercount = 0; |
| 137 | req->deductheadercount = 0; |
| 138 | req->headerline = 0; |
| 139 | req->offset = 0; |
| 140 | req->httpcode = 0; |
| 141 | req->keepon = 0; |
| 142 | req->upgr101 = UPGR101_INIT; |
| 143 | req->sendbuf_hds_len = 0; |
| 144 | req->timeofdoc = 0; |
| 145 | req->location = NULL; |
| C1: Curl_req_hard_reset sets req->location = NULL without freeing (master) | |
| 146 | req->newurl = NULL; |
| 147 | #ifndef CURL_DISABLE_COOKIES |
| 148 | req->setcookies = 0; |
| 149 | #endif |
| 150 | req->header = FALSE; |
| 151 | req->content_range = FALSE; |
| 152 | req->download_done = FALSE; |
| 153 | req->eos_written = FALSE; |
| 154 | req->eos_read = FALSE; |
| 155 | req->eos_sent = FALSE; |
| 156 | req->upload_done = FALSE; |
| 157 | req->upload_aborted = FALSE; |
| 158 | req->ignorebody = FALSE; |
| 159 | req->http_bodyless = FALSE; |
| 160 | req->chunk = FALSE; |
| 161 | req->ignore_cl = FALSE; |
| 162 | req->upload_chunky = FALSE; |
| 163 | req->getheader = FALSE; |
| 164 | req->no_body = data->set.opt_no_body; |
| 165 | req->authneg = FALSE; |
| 166 | req->shutdown = FALSE; |
| 167 | } |
| 283 | char *Curl_copy_header_value(const char *header) |
| 284 | { |
| 285 | struct Curl_str out; |
| 286 | |
| 287 | /* find the end of the header name */ |
| 288 | if(!Curl_str_cspn(&header, &out, ";:") && |
| 289 | (!Curl_str_single(&header, ':') || !Curl_str_single(&header, ';'))) { |
| 290 | Curl_str_untilnl(&header, &out, MAX_HTTP_RESP_HEADER_SIZE); |
| 291 | Curl_str_trimblanks(&out); |
| 292 | |
| 293 | return Curl_memdup0(Curl_str(&out), Curl_strlen(&out)); |
| C1: Curl_copy_header_value allocates via Curl_memdup0 (malloc) | |
| 294 | } |
| 295 | /* bad input */ |
| 296 | return NULL; |
| 297 | } |
| 112 | void *Curl_memdup0(const char *src, size_t length) |
| 113 | { |
| 114 | char *buf = malloc(length + 1); |
| C1: Curl_memdup0 allocates with malloc(length + 1) | |
| 115 | if(!buf) |
| 116 | return NULL; |
| 117 | if(length) { |
| 118 | DEBUGASSERT(src); /* must never be NULL */ |
| 119 | memcpy(buf, src, length); |
| 120 | } |
| 121 | buf[length] = 0; |
| 122 | return buf; |
| 123 | } |
| 169 | void Curl_req_free(struct SingleRequest *req, struct Curl_easy *data) |
| 170 | { |
| 171 | Curl_safefree(req->newurl); |
| C1: Curl_req_free frees req->newurl and req->p.ftp but not req->location (master) | |
| 172 | if(req->sendbuf_init) |
| 173 | Curl_bufq_free(&req->sendbuf); |
| 174 | Curl_client_cleanup(data); |
| 175 | } |
| 172 | void Curl_req_free(struct SingleRequest *req, struct Curl_easy *data) |
| 173 | { |
| 174 | /* This is a bit ugly. `req->p` is a union and we assume we can |
| 175 | * free this safely without leaks. */ |
| 176 | Curl_safefree(req->p.ftp); |
| 177 | Curl_safefree(req->newurl); |
| C1: Curl_req_free frees req->newurl and req->p.ftp but not req->location (release) | |
| 178 | if(req->sendbuf_init) |
| 179 | Curl_bufq_free(&req->sendbuf); |
| 180 | Curl_client_cleanup(data); |
| 181 | |
| 182 | #ifndef CURL_DISABLE_DOH |
| 183 | Curl_doh_cleanup(data); |
| 184 | #endif |
| 185 | } |
| 117 | void Curl_req_hard_reset(struct SingleRequest *req, struct Curl_easy *data) |
| 118 | { |
| 119 | struct curltime t0 = {0, 0}; |
| 120 | |
| 121 | /* This is a bit ugly. `req->p` is a union and we assume we can |
| 122 | * free this safely without leaks. */ |
| 123 | Curl_safefree(req->p.ftp); |
| 124 | Curl_safefree(req->newurl); |
| 125 | Curl_client_reset(data); |
| 126 | if(req->sendbuf_init) |
| 127 | Curl_bufq_reset(&req->sendbuf); |
| 128 | |
| 129 | #ifndef CURL_DISABLE_DOH |
| 130 | Curl_doh_close(data); |
| 131 | #endif |
| 132 | /* Can no longer memset() this struct as we need to keep some state */ |
| 133 | req->size = -1; |
| 134 | req->maxdownload = -1; |
| 135 | req->bytecount = 0; |
| 136 | req->writebytecount = 0; |
| 137 | req->start = t0; |
| 138 | req->headerbytecount = 0; |
| 139 | req->allheadercount = 0; |
| 140 | req->deductheadercount = 0; |
| 141 | req->headerline = 0; |
| 142 | req->offset = 0; |
| 143 | req->httpcode = 0; |
| 144 | req->keepon = 0; |
| 145 | req->upgr101 = UPGR101_INIT; |
| 146 | req->sendbuf_hds_len = 0; |
| 147 | req->timeofdoc = 0; |
| 148 | req->location = NULL; |
| C1: Curl_req_hard_reset sets req->location = NULL without freeing (release curl-8_13_0) | |
| 149 | req->newurl = NULL; |
| 150 | #ifndef CURL_DISABLE_COOKIES |
| 151 | req->setcookies = 0; |
| 152 | #endif |
| 153 | req->header = FALSE; |
| 154 | req->content_range = FALSE; |
| 155 | req->download_done = FALSE; |
| 156 | req->eos_written = FALSE; |
| 157 | req->eos_read = FALSE; |
| 158 | req->eos_sent = FALSE; |
| 159 | req->upload_done = FALSE; |
| 160 | req->upload_aborted = FALSE; |
| 161 | req->ignorebody = FALSE; |
| 162 | req->http_bodyless = FALSE; |
| 163 | req->chunk = FALSE; |
| 164 | req->ignore_cl = FALSE; |
| 165 | req->upload_chunky = FALSE; |
| 166 | req->getheader = FALSE; |
| 167 | req->no_body = data->set.opt_no_body; |
| 168 | req->authneg = FALSE; |
| 169 | req->shutdown = FALSE; |
| 170 | } |
| 106 | static CURLcode http_header(struct Curl_easy *data, |
| C2: Only declaration of http_header shown; implementation not provided | |
| C3: http_header implementation not provided | |
| 107 | const char *hd, size_t hdlen); |
| 108 | static CURLcode http_host(struct Curl_easy *data, struct connectdata *conn); |
| 109 | static CURLcode http_range(struct Curl_easy *data, |
| 110 | Curl_HttpReq httpreq); |
| 111 | static CURLcode http_req_complete(struct Curl_easy *data, |
| 112 | struct dynbuf *r, int httpversion, |
| 113 | Curl_HttpReq httpreq); |
| 114 | static CURLcode http_req_set_reader(struct Curl_easy *data, |
| 115 | Curl_HttpReq httpreq, int httpversion, |
| 116 | const char **tep); |
| 117 | static CURLcode http_size(struct Curl_easy *data); |
| 118 | static CURLcode http_statusline(struct Curl_easy *data, |
| 119 | struct connectdata *conn); |
| 120 | static CURLcode http_target(struct Curl_easy *data, struct connectdata *conn, |
| 121 | struct dynbuf *req); |
| 122 | static CURLcode http_useragent(struct Curl_easy *data); |
| 123 | #ifdef HAVE_LIBZ |
| 124 | static CURLcode http_transferencode(struct Curl_easy *data); |
| 125 | #endif |
| 126 | |
| 127 | |
| 128 | /* |
| 129 | * HTTP handler interface. |
| 130 | */ |
| 131 | const struct Curl_handler Curl_handler_http = { |
| 132 | "http", /* scheme */ |
| 133 | Curl_http_setup_conn, /* setup_connection */ |
| 134 | Curl_http, /* do_it */ |
| 135 | Curl_http_done, /* done */ |
| 136 | ZERO_NULL, /* do_more */ |
| 137 | Curl_http_connect, /* connect_it */ |
| 138 | ZERO_NULL, /* connecting */ |
| 139 | ZERO_NULL, /* doing */ |
| 140 | ZERO_NULL, /* proto_getsock */ |
| 141 | Curl_http_getsock_do, /* doing_getsock */ |
| 142 | ZERO_NULL, /* domore_getsock */ |
| 143 | ZERO_NULL, /* perform_getsock */ |
| 144 | ZERO_NULL, /* disconnect */ |
| 145 | Curl_http_write_resp, /* write_resp */ |
| 146 | Curl_http_write_resp_hd, /* write_resp_hd */ |
| 147 | ZERO_NULL, /* connection_check */ |
| 148 | ZERO_NULL, /* attach connection */ |
| 149 | Curl_http_follow, /* follow */ |
| 150 | PORT_HTTP, /* defport */ |
| 151 | CURLPROTO_HTTP, /* protocol */ |
| 152 | CURLPROTO_HTTP, /* family */ |
| 153 | PROTOPT_CREDSPERREQUEST | /* flags */ |
| 154 | PROTOPT_USERPWDCTRL |
| 155 | }; |
Show 51 lines (not cited by the judge)
| 2317 | *announced_exp100 = TRUE; |
| 2318 | } |
| 2319 | } |
| 2320 | return CURLE_OK; |
| 2321 | } |
| 2322 | |
| 2323 | static CURLcode http_req_complete(struct Curl_easy *data, |
| 2324 | struct dynbuf *r, int httpversion, |
| 2325 | Curl_HttpReq httpreq) |
| 2326 | { |
| 2327 | CURLcode result = CURLE_OK; |
| 2328 | curl_off_t req_clen; |
| 2329 | bool announced_exp100 = FALSE; |
| 2330 | |
| 2331 | DEBUGASSERT(data->conn); |
| 2332 | if(data->req.upload_chunky) { |
| 2333 | result = Curl_httpchunk_add_reader(data); |
| 2334 | if(result) |
| 2335 | return result; |
| 2336 | } |
| 2337 | |
| 2338 | /* Get the request body length that has been set up */ |
| 2339 | req_clen = Curl_creader_total_length(data); |
| 2340 | switch(httpreq) { |
| 2341 | case HTTPREQ_PUT: |
| 2342 | case HTTPREQ_POST: |
| 2343 | #if !defined(CURL_DISABLE_MIME) || !defined(CURL_DISABLE_FORM_API) |
| 2344 | case HTTPREQ_POST_FORM: |
| 2345 | case HTTPREQ_POST_MIME: |
| 2346 | #endif |
| 2347 | /* We only set Content-Length and allow a custom Content-Length if |
| 2348 | we do not upload data chunked, as RFC2616 forbids us to set both |
| 2349 | kinds of headers (Transfer-Encoding: chunked and Content-Length). |
| 2350 | We do not override a custom "Content-Length" header, but during |
| 2351 | authentication negotiation that header is suppressed. |
| 2352 | */ |
| 2353 | if(req_clen >= 0 && !data->req.upload_chunky && |
| 2354 | (data->req.authneg || |
| 2355 | !Curl_checkheaders(data, STRCONST("Content-Length")))) { |
| 2356 | /* we allow replacing this header if not during auth negotiation, |
| 2357 | although it is not very wise to actually set your own */ |
| 2358 | result = curlx_dyn_addf(r, "Content-Length: %" FMT_OFF_T "\r\n", |
| 2359 | req_clen); |
| 2360 | } |
| 2361 | if(result) |
| 2362 | goto out; |
| 2363 | |
| 2364 | #ifndef CURL_DISABLE_MIME |
| 2365 | /* Output mime-generated headers. */ |
| 2366 | if(data->state.mimepost && |
| 2367 | ((httpreq == HTTPREQ_POST_FORM) || (httpreq == HTTPREQ_POST_MIME))) { |
Show 15 lines (not cited by the judge)
| 283 | char *Curl_copy_header_value(const char *header) |
| 284 | { |
| 285 | struct Curl_str out; |
| 286 | |
| 287 | /* find the end of the header name */ |
| 288 | if(!curlx_str_cspn(&header, &out, ";:") && |
| 289 | (!curlx_str_single(&header, ':') || !curlx_str_single(&header, ';'))) { |
| 290 | curlx_str_untilnl(&header, &out, MAX_HTTP_RESP_HEADER_SIZE); |
| 291 | curlx_str_trimblanks(&out); |
| 292 | |
| 293 | return Curl_memdup0(curlx_str(&out), curlx_strlen(&out)); |
| 294 | } |
| 295 | /* bad input */ |
| 296 | return NULL; |
| 297 | } |