✓ProveIt
Examples / Report #2956023 Saved analysis

CVE-2025-0725: gzip integer overflow

curl · 2025-01-2332.9s analysis · $0.030 in model calls

What the report claims

The report claims that libcurl's gzip handling in content_encoding.c has an integer overflow leading to heap corruption and arbitrary free() of attacker-controlled data when Content-Encoding: gzip is used.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2025-0725 gzip integer overflow

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Needs a policy review

The code does what the report says, but curl's own policy may exclude it. A maintainer decides.

curl policy: Not practically possible
Judgment withdrawn. The answer failed the evidence check.
See the withdrawn model reasoning

The following reasoning failed the citation check and does not support the final finding.

  • All six claims are technically correct for the code path under #ifdef OLD_ZLIB_SUPPORT
  • However, this code is only compiled when OLD_ZLIB_SUPPORT is defined, which supports zlib versions < 1.2.0.4 (released 2003)
  • The comment at line 474-476 states 'Support for old zlib versions is compiled away' when OLD_ZLIB_SUPPORT is not defined, indicating the default build excludes this path
  • No modern system uses zlib that old; the feature is effectively obsolete and not enabled in standard installations
  • Per project policy 'Not practically possible: If the flaw or vulnerability cannot practically get executed on existing hardware it is not a security problem'
What still needs checking
  • Build configuration default for OLD_ZLIB_SUPPORT (not provided in sources)
How this analysis ran 6 claims · 8 citations verified

Evidence check: the behavior is real; the project's policy section 'Not practically possible' may exclude it

Checked against curl permalink 7e814c871793 (7e814c871793). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 10 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
6 claims · 2287 tokens · $0.0002
3 · Evidence
6 code snippets
real curl source at 7e814c871793 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
17539 tokens · 30.1s · $0.0299
5 · Receipts
8 of 8 citations verified
every citation must point at code the judge was shown
Inspect the withdrawn draft reply

This draft belongs to the withdrawn judgment. It may contain unsupported claims.

Thank you for the detailed report. The code you cite does contain the integer overflow and subsequent heap corruption you describe, but only in the code path guarded by #ifdef OLD_ZLIB_SUPPORT. This path exists solely to support zlib versions older than 1.2.0.4 (from 2003). In default builds this code is compiled away (see lines 473-476), and no modern system runs such an ancient zlib. Per our policy, vulnerabilities that cannot practically be executed on existing hardware — including those in obsolete, off-by-default compatibility layers — are not considered security issues. If you have evidence that OLD_ZLIB_SUPPORT is enabled by default in common distributions, please share it.

Inspect all claims and source code 6 claims · 6 code excerpts

This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.

Report claims

C1flaw supported

integer overflow of z->avail_in in line 533

Read the reasoning

The code adds nbytes to z->avail_in without any bounds check. Since z->avail_in is a 32-bit unsigned integer (uInt) and nbytes is size_t (64-bit on modern systems), repeated additions can wrap around.

C2flaw supported

Curl_saferealloc() called with overflowed size causing shrink of z->next_in

Read the reasoning

If z->avail_in has overflowed to a small value, Curl_saferealloc will shrink the allocation to that small size.

C3flaw supported

memcpy() writes out-of-bounds before z->next_in after shrink

Read the reasoning

After overflow, z->avail_in is small while nbytes is large. The expression z->avail_in - nbytes underflows in unsigned arithmetic, producing a huge positive offset that writes far before the buffer start.

C4flaw supported

free(z->next_in) puts forged chunk into allocator freelist

Read the reasoning

When check_gzip_header returns GZIP_OK, the code frees z->next_in which now points to a heap chunk with corrupted metadata due to the preceding OOB write.

C5flaw supported

z->avail_in can be repeatedly incremented by attacker data causing endless calls to gzip_do_write()

Read the reasoning

When check_gzip_header returns GZIP_UNDERFLOW (e.g., waiting for NUL-terminated filename), the function returns success and remains in ZLIB_GZIP_HEADER state, allowing the attacker to send unlimited data that increments z->avail_in each time.

C6flaw supported

attacker can trigger integer overflow of z->avail_in with endlessly large gzip headers

Read the reasoning

By setting the ORIG_NAME flag (flag 8) and sending a very long filename without a NUL terminator, check_gzip_header returns GZIP_UNDERFLOW repeatedly, keeping the parser in ZLIB_GZIP_HEADER state and allowing unlimited growth of z->avail_in until overflow.

Read the original report (excerpt)

Full report on HackerOne ↗

Hello, no AI slop this time. I promise! The current master branch of [libcurl](https://github.com/curl/curl/tree/7e814c8717939393d4436d75f5f0c3ffa98c8c53) contains a vulnerability in [lib/content_encoding.c](https://github.com/curl/curl/blob/7e814c8717939393d4436d75f5f0c3ffa98c8c53/lib/content_encoding.c#L539) that allows a malicious HTTP-server to craft an arbitrary heap chunk in the memory of the victim and issue a `free()` of that forged chunk, when `Content-Encoding: gzip` is in use. The vulnerability is in function `gzip_do_write()` in lines 533 - 544: ```c z->avail_in += (uInt) nbytes; z->next_in = Curl_saferealloc(z->next_in, z->avail_in); if(!z->next_in) { return exit_zlib(data, z, &zp->zlib_init, CURLE_OUT_OF_MEMORY); } /* Append the new block of data to the previous one */ memcpy(z->next_in + z->avail_in - nbytes, buf, nbytes); switch(check_gzip_header(z->next_in, (ssize_t)z->avail_in, &hlen)) { case GZIP_OK: /* This is the zlib stream data */ free(z->next_in); ``` On systems with a zlib version < `1.2.0.4`, libcurl offers to manually parse gzip headers and trailers instead of passing everything to zlib as is. Unfortunately, when parsing the headers the remote server can trigger an integer overflow of `z->avail_in` in line 533, which leads the following `Curl_saferealloc()` call to shrink the chunk `z->next_in`. The following `memcpy()` can then write out-of-bounds before `z->next_in`, overwriting chunk metadata of the allocator. And right after the oob-write was triggered, the call to `free(z->next_in)` puts the forged chunk into the freelist of the allocator. `z->avail_in` can be overflowed because libcurl supports endlessly large gzip headers that can lead to repeated calls of `gzip_do_write()` with `zp->zlib_init == ZLIB_GZIP_HEADER` that keep incrementing `z->avail_in` with the amount of received data `nbytes`. # PoC Consider the following malicious HTTP-server: ```py #!/usr/bin/env python3 from pwn import * gzip_header = bytes([ 0x1f, 0x8b, # magic values 8, # method 8, # flags 0, 0, 0, 0, 0, 0, # random bullshit go ]) with listen(1234) as conn: conn.wait_for_connection() # Discard HTTP request while True: line = conn.recvline() if line == b"\r\n": break # Fill up buffer conn.sendline(b"HTTP/1.1 200 OK\ …

Source code

lib/content_encoding.clines 455–584 · lines 539-539 cited by the report; definition of gzip_do_write
455static CURLcode gzip_do_write(struct Curl_easy *data,
456 struct Curl_cwriter *writer, int type,
457 const char *buf, size_t nbytes)
458{
459 struct zlib_writer *zp = (struct zlib_writer *) writer;
460 z_stream *z = &zp->z; /* zlib state structure */
461
462 if(!(type & CLIENTWRITE_BODY) || !nbytes)
463 return Curl_cwriter_write(data, writer->next, type, buf, nbytes);
464
465 if(zp->zlib_init == ZLIB_INIT_GZIP) {
466 /* Let zlib handle the gzip decompression entirely */
467 z->next_in = (Bytef *) buf;
468 z->avail_in = (uInt) nbytes;
469 /* Now uncompress the data */
470 return inflate_stream(data, writer, type, ZLIB_INIT_GZIP);
471 }
472
473#ifndef OLD_ZLIB_SUPPORT
474 /* Support for old zlib versions is compiled away and we are running with
475 an old version, so return an error. */
476 return exit_zlib(data, z, &zp->zlib_init, CURLE_WRITE_ERROR);
477
478#else
479 /* This next mess is to get around the potential case where there is not
480 * enough data passed in to skip over the gzip header. If that happens, we
481 * malloc a block and copy what we have then wait for the next call. If
482 * there still is not enough (this is definitely a worst-case scenario), we
483 * make the block bigger, copy the next part in and keep waiting.
484 *
485 * This is only required with zlib versions < 1.2.0.4 as newer versions
486 * can handle the gzip header themselves.
487 */
488
489 switch(zp->zlib_init) {
490 /* Skip over gzip header? */
491 case ZLIB_INIT:
492 {
493 /* Initial call state */
494 ssize_t hlen;
495
496 switch(check_gzip_header((unsigned char *) buf, nbytes, &hlen)) {
497 case GZIP_OK:
498 z->next_in = (Bytef *) buf + hlen;
499 z->avail_in = (uInt) (nbytes - hlen);
500 zp->zlib_init = ZLIB_GZIP_INFLATING; /* Inflating stream state */
501 break;
502
503 case GZIP_UNDERFLOW:
504 /* We need more data so we can find the end of the gzip header. it is
505 * possible that the memory block we malloc here will never be freed if
506 * the transfer abruptly aborts after this point. Since it is unlikely
507 * that circumstances will be right for this code path to be followed in
508 * the first place, and it is even more unlikely for a transfer to fail
509 * immediately afterwards, it should seldom be a problem.
510 */
511 z->avail_in = (uInt) nbytes;
512 z->next_in = malloc(z->avail_in);
513 if(!z->next_in) {
514 return exit_zlib(data, z, &zp->zlib_init, CURLE_OUT_OF_MEMORY);
515 }
516 memcpy(z->next_in, buf, z->avail_in);
517 zp->zlib_init = ZLIB_GZIP_HEADER; /* Need more gzip header data state */
518 /* We do not have any data to inflate yet */
519 return CURLE_OK;
520
521 case GZIP_BAD:
522 default:
523 return exit_zlib(data, z, &zp->zlib_init, process_zlib_error(data, z));
524 }
525
526 }
527 break;
528
529 case ZLIB_GZIP_HEADER:
530 {
531 /* Need more gzip header data state */
532 ssize_t hlen;
533 z->avail_in += (uInt) nbytes;
C1: z->avail_in += (uInt) nbytes; with no overflow check; z->avail_in is uInt (32-bit), nbytes is size_t
C5: Each call adds nbytes to z->avail_in
C6: z->avail_in += (uInt) nbytes; in ZLIB_GZIP_HEADER case
534 z->next_in = Curl_saferealloc(z->next_in, z->avail_in);
C2: z->next_in = Curl_saferealloc(z->next_in, z->avail_in); called with potentially wrapped-around size
535 if(!z->next_in) {
536 return exit_zlib(data, z, &zp->zlib_init, CURLE_OUT_OF_MEMORY);
537 }
538 /* Append the new block of data to the previous one */
539 memcpy(z->next_in + z->avail_in - nbytes, buf, nbytes);
C3: memcpy(z->next_in + z->avail_in - nbytes, buf, nbytes); with small z->avail_in and large nbytes causes unsigned wraparound
540
541 switch(check_gzip_header(z->next_in, (ssize_t)z->avail_in, &hlen)) {
542 case GZIP_OK:
543 /* This is the zlib stream data */
544 free(z->next_in);
C4: free(z->next_in); frees the corrupted pointer after the OOB write
545 /* Do not point into the malloced block since we just freed it */
546 z->next_in = (Bytef *) buf + hlen + nbytes - z->avail_in;
547 z->avail_in = z->avail_in - (uInt)hlen;
548 zp->zlib_init = ZLIB_GZIP_INFLATING; /* Inflating stream state */
549 break;
550
551 case GZIP_UNDERFLOW:
552 /* We still do not have any data to inflate! */
553 return CURLE_OK;
C5: case GZIP_UNDERFLOW: return CURLE_OK; keeps state as ZLIB_GZIP_HEADER for more data
554
555 case GZIP_BAD:
556 default:
557 return exit_zlib(data, z, &zp->zlib_init, process_zlib_error(data, z));
558 }
559
560 }
561 break;
562
563 case ZLIB_EXTERNAL_TRAILER:
564 z->next_in = (Bytef *) buf;
565 z->avail_in = (uInt) nbytes;
566 return process_trailer(data, zp);
567
568 case ZLIB_GZIP_INFLATING:
569 default:
570 /* Inflating stream state */
571 z->next_in = (Bytef *) buf;
572 z->avail_in = (uInt) nbytes;
573 break;
574 }
575
576 if(z->avail_in == 0) {
577 /* We do not have any data to inflate; wait until next time */
578 return CURLE_OK;
579 }
580
581 /* We have parsed the header, now uncompress the data */
582 return inflate_stream(data, writer, type, ZLIB_GZIP_INFLATING);
583#endif
584}
lib/content_encoding.clines 376–452 · definition of check_gzip_header
376static gzip_status check_gzip_header(unsigned char const *data, ssize_t len,
377 ssize_t *headerlen)
378{
379 int method, flags;
380 const ssize_t totallen = len;
381
382 /* The shortest header is 10 bytes */
383 if(len < 10)
384 return GZIP_UNDERFLOW;
385
386 if((data[0] != GZIP_MAGIC_0) || (data[1] != GZIP_MAGIC_1))
387 return GZIP_BAD;
388
389 method = data[2];
390 flags = data[3];
391
392 if(method != Z_DEFLATED || (flags & CURL_GZIPFLAG_RESERVED) != 0) {
393 /* cannot handle this compression method or unknown flag */
394 return GZIP_BAD;
395 }
396
397 /* Skip over time, xflags, OS code and all previous bytes */
398 len -= 10;
399 data += 10;
400
401 if(flags & CURL_GZIPFLAG_EXTRA_FIELD) {
402 ssize_t extra_len;
403
404 if(len < 2)
405 return GZIP_UNDERFLOW;
406
407 extra_len = (data[1] << 8) | data[0];
408
409 if(len < (extra_len + 2))
410 return GZIP_UNDERFLOW;
411
412 len -= (extra_len + 2);
413 data += (extra_len + 2);
414 }
415
416 if(flags & CURL_GZIPFLAG_ORIG_NAME) {
C6: if(flags & ORIG_NAME) { while(len && *data) { --len; ++data; } if(!len || *data) return GZIP_UNDERFLOW; }
417 /* Skip over NUL-terminated filename */
418 while(len && *data) {
419 --len;
420 ++data;
421 }
422 if(!len || *data)
423 return GZIP_UNDERFLOW;
424
425 /* Skip over the NUL */
426 --len;
427 ++data;
428 }
429
430 if(flags & CURL_GZIPFLAG_COMMENT) {
431 /* Skip over NUL-terminated comment */
432 while(len && *data) {
433 --len;
434 ++data;
435 }
436 if(!len || *data)
437 return GZIP_UNDERFLOW;
438
439 /* Skip over the NUL */
440 --len;
441 }
442
443 if(flags & CURL_GZIPFLAG_HEAD_CRC) {
444 if(len < 2)
445 return GZIP_UNDERFLOW;
446
447 len -= 2;
448 }
449
450 *headerlen = totallen - len;
451 return GZIP_OK;
452}
lib/strdup.clines 136–143 · definition of Curl_saferealloc
Show 8 lines (not cited by the judge)
136void *Curl_saferealloc(void *ptr, size_t size)
137{
138 void *datap = realloc(ptr, size);
139 if(size && !datap)
140 /* only free 'ptr' if size was non-zero */
141 free(ptr);
142 return datap;
143}
lib/curl_memory.hlines 163–163 · definition of free
Show 1 lines (not cited by the judge)
163#define free(ptr) Curl_cfree(ptr)
lib/content_encoding.c@curl-8_11_1lines 374–450 · definition of check_gzip_header as in release curl-8_11_1; differs from master; commits that changed it since: 24ef9de9f4 content_encoding: namespace GZIP flag constants
Show 77 lines (not cited by the judge)
374static gzip_status check_gzip_header(unsigned char const *data, ssize_t len,
375 ssize_t *headerlen)
376{
377 int method, flags;
378 const ssize_t totallen = len;
379
380 /* The shortest header is 10 bytes */
381 if(len < 10)
382 return GZIP_UNDERFLOW;
383
384 if((data[0] != GZIP_MAGIC_0) || (data[1] != GZIP_MAGIC_1))
385 return GZIP_BAD;
386
387 method = data[2];
388 flags = data[3];
389
390 if(method != Z_DEFLATED || (flags & RESERVED) != 0) {
391 /* cannot handle this compression method or unknown flag */
392 return GZIP_BAD;
393 }
394
395 /* Skip over time, xflags, OS code and all previous bytes */
396 len -= 10;
397 data += 10;
398
399 if(flags & EXTRA_FIELD) {
400 ssize_t extra_len;
401
402 if(len < 2)
403 return GZIP_UNDERFLOW;
404
405 extra_len = (data[1] << 8) | data[0];
406
407 if(len < (extra_len + 2))
408 return GZIP_UNDERFLOW;
409
410 len -= (extra_len + 2);
411 data += (extra_len + 2);
412 }
413
414 if(flags & ORIG_NAME) {
415 /* Skip over NUL-terminated filename */
416 while(len && *data) {
417 --len;
418 ++data;
419 }
420 if(!len || *data)
421 return GZIP_UNDERFLOW;
422
423 /* Skip over the NUL */
424 --len;
425 ++data;
426 }
427
428 if(flags & COMMENT) {
429 /* Skip over NUL-terminated comment */
430 while(len && *data) {
431 --len;
432 ++data;
433 }
434 if(!len || *data)
435 return GZIP_UNDERFLOW;
436
437 /* Skip over the NUL */
438 --len;
439 }
440
441 if(flags & HEAD_CRC) {
442 if(len < 2)
443 return GZIP_UNDERFLOW;
444
445 len -= 2;
446 }
447
448 *headerlen = totallen - len;
449 return GZIP_OK;
450}
lib/content_encoding.clines 138–152 · definition of exit_zlib
Show 15 lines (not cited by the judge)
138static CURLcode
139exit_zlib(struct Curl_easy *data,
140 z_stream *z, zlibInitState *zlib_init, CURLcode result)
141{
142 if(*zlib_init == ZLIB_GZIP_HEADER)
143 Curl_safefree(z->next_in);
144
145 if(*zlib_init != ZLIB_UNINIT) {
146 if(inflateEnd(z) != Z_OK && result == CURLE_OK)
147 result = process_zlib_error(data, z);
148 *zlib_init = ZLIB_UNINIT;
149 }
150
151 return result;
152}