CVE-2025-0725: gzip integer overflow
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
The code does what the report says, but curl's own policy may exclude it. A maintainer decides.
curl policy: Not practically possibleSee 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.
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
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.
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.
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.
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.
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.
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)
Source code
| 455 | static 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 | } |
| 376 | static 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 | } |
Show 8 lines (not cited by the judge)
| 136 | void *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 | } |
Show 1 lines (not cited by the judge)
| 163 | #define free(ptr) Curl_cfree(ptr) |
Show 77 lines (not cited by the judge)
| 374 | static 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 | } |
Show 15 lines (not cited by the judge)
| 138 | static CURLcode |
| 139 | exit_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 | } |