✓ProveIt
Examples / Report #3506159 Saved analysis
The AI's own mistake

The AI believed a made-up quote.

Heap Out-of-Bounds Read in lib/http2.c via Malformed PUSH_PROMISE Headers

curl · 2026-01-1021.4s analysis · $0.018 in model calls

What the report claims

The report relies on a claim about how nghttp2 handles HTTP headers.

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

Needs human review

The model accepted a documentation quote that the fetched source did not contain. The citation check withdrew its judgment and asked for human review.

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.

  • lib/http2.c:1642 uses %s format specifiers on name/value pointers from nghttp2 without using the provided namelen/valuelen lengths
  • nghttp2 documentation explicitly states name/value pointers are NOT guaranteed to be null-terminated and applications MUST use length parameters
  • curl_maprintf processes %s by calling strlen, which reads past the buffer boundary when no null terminator exists
  • The fix is straightforward: use %.*s with the length parameters as already done correctly for trailers at line 1650-1651
  • This is reachable by a malicious HTTP/2 server sending PUSH_PROMISE headers, causing heap out-of-bounds read (information disclosure or crash)
How this analysis ran 5 claims · 12 citations verified

Evidence check: judge quoted an external source that does not say that (https://nghttp2.org/documentation/nghttp2.h.html)

Checked against curl master on 2026-01-10 (f4b16563ab74). 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
partially grounded
no model · 8 references checked out, 11 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2437 tokens · $0.0003
3 · Evidence
3 code snippets
real curl source at f4b16563ab74 · 3 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
12748 tokens · 15.4s · $0.0174
5 · Receipts
12 of 12 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 vulnerability is confirmed: lib/http2.c:1642 uses `%s:%s` with nghttp2 header pointers that are not guaranteed null-terminated, while the callback provides `namelen` and `valuelen`. The fix using `%.*s:%.*s` with the length parameters is correct and matches the pattern already used for trailers at line 1650. We will apply this fix.

Inspect all claims and source code 5 claims · 3 code excerpts

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

Report claims

C1flaw supported

curl_maprintf is called with %s format specifiers on header name and value pointers that are not null-terminated, causing strlen() to read past buffer boundaries

Read the reasoning

The code at lib/http2.c:1642 passes name and value pointers to curl_maprintf with %s format specifiers. Since nghttp2 does not guarantee null-termination (per external source), and curl_maprintf uses strlen for %s, this causes an out-of-bounds read when the buffers lack a terminating null byte.

C2location supported

on_header callback in lib/http2.c around line 1642

Read the reasoning

The deterministic checks confirm lib/http2.c exists and line 1642 contains the curl_maprintf call within the on_header function.

C3trigger supported

sending a PUSH_PROMISE header with a name and value that lack null termination, provided by nghttp2 via its callback parameters namelen and valuelen

Read the reasoning

The on_header callback is registered for nghttp2 header events. When a PUSH_PROMISE frame arrives (frame type HEADERS with category NGHTTP2_HCAT_PUSH_PROMISE), the code at line 1642 processes the header name/value using %s without length limits.

C4impact supported

out-of-bounds read of heap memory adjacent to the header buffer, potentially disclosing sensitive data or causing a crash

Read the reasoning

The ASAN log in the report shows a heap-buffer-overflow in strlen called from curl_maprintf at lib/http2.c:1642. An out-of-bounds read can disclose adjacent heap memory (information disclosure) or hit unmapped memory (crash/DOS).

C5reproduction supported

run an HTTP/2 server that pushes a stream with a header whose name and value are not null-terminated, then invoke curl with --http2-prior-knowledge against that server while ASAN is enabled

Read the reasoning

The reproduction steps are plausible: a malicious HTTP/2 server sends PUSH_PROMISE headers with non-null-terminated name/value buffers. The deterministic checks show the ungrounded symbols (H2Configuration, etc.) belong to the Python test script (h2 library), not curl itself, so they don't affect the curl code assessment.

Deterministic checks that failed

  • symbol H2Configuration() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol H2Connection() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol initiate_connection() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol update_settings() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol data_to_send() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol receive_data() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol isinstance() does not exist in curl at f4b16563ab74 or in any release since 7.50
  • symbol send_headers() does not exist in curl at f4b16563ab74 or in any release since 7.50

What the report says about other projects

confirmed The 'name' and 'value' pointers are NOT guaranteed to be null-terminated. Applications MUST use the provided length parameters.

“The 'name' and 'value' pointers are NOT guaranteed to be null-terminated. Applications MUST use the provided length parameters.” source

Read the original report (excerpt)

Full report on HackerOne ↗

Summary A heap-based out-of-bounds read vulnerability exists in libcurl's HTTP/2 implementation. The on_header callback in lib/http2.c incorrectly treats header names and values provided by nghttp2 as null-terminated C-strings. Specifically, passing these pointers to curl_maprintf with the %s format specifier triggers an out-of-bounds read via strlen(), as nghttp2 provides raw byte buffers with explicit lengths, not null-terminated strings. +1 Vulnerability Analysis & Root Cause 1. API Contract Violation: According to the nghttp2 documentation, the nghttp2_on_header_callback provides pointers to the header name and value (name, value) alongside their lengths (namelen, valuelen). The documentation explicitly states: "The 'name' and 'value' pointers are NOT guaranteed to be null-terminated. [...] Applications MUST use the provided length parameters.". 2. Vulnerable Implementation: In lib/http2.c, the on_header function ignores the length parameters when formatting the string for PUSH_PROMISE headers, relying on curl_maprintf which internally uses strlen: C /* lib/http2.c around line 1642 - Vulnerable */ h = curl_maprintf("%s:%s", name, value); 3. Execution Flow: curl_maprintf parses the %s format specifier. It internally calls strlen() on the name and value pointers. Since the malicious server sends a header without a null byte, strlen() reads past the allocated buffer boundary until it hits a coincidental null byte in adjacent heap memory. This results in an Out-of-Bounds Read. Steps to Reproduce 1. Build Environment Compile cURL with AddressSanitizer (ASAN) to visualize the memory violation: Bash ./configure --with-nghttp2 --enable-debug CFLAGS="-fsanitize=address -g" LDFLAGS="-fsanitize=address" make -j$(nproc) 2. Reproduction Script (http2_server.py) This Python script (using h2) establishes an HTTP/2 connection and pushes a stream with a non-null-terminated header. Python import asyncio, h2.connection, h2.events, h2.config async def handle(reader, writer): config = h2.config.H2Configuration(client_side=False) conn = h2.connection.H2Connection(config=config) conn.initiate_connection() conn.update_settings({h2.settings.SettingCodes.ENABLE_PUSH: 1}) writer.write(conn.data_to_send()) await writer.drain() data = await reader.read(65535) events = conn.receive_data(data) for event in events: if is …

Source code

lib/http2.clines 1617–1667 · lines 1642-1642 cited by the report
1617 stream->push_headers_alloc = 10;
C3: Code context shows this is in the PUSH_PROMISE handling branch (push_headers allocation)
1618 stream->push_headers = curlx_malloc(stream->push_headers_alloc *
1619 sizeof(char *));
1620 if(!stream->push_headers)
1621 return NGHTTP2_ERR_CALLBACK_FAILURE;
1622 stream->push_headers_used = 0;
1623 }
1624 else if(stream->push_headers_used ==
1625 stream->push_headers_alloc) {
1626 char **headp;
1627 if(stream->push_headers_alloc > 1000) {
1628 /* this is beyond crazy many headers, bail out */
1629 failf(data_s, "Too many PUSH_PROMISE headers");
1630 free_push_headers(stream);
1631 return NGHTTP2_ERR_CALLBACK_FAILURE;
1632 }
1633 stream->push_headers_alloc *= 2;
1634 headp = curlx_realloc(stream->push_headers,
1635 stream->push_headers_alloc * sizeof(char *));
1636 if(!headp) {
1637 free_push_headers(stream);
1638 return NGHTTP2_ERR_CALLBACK_FAILURE;
1639 }
1640 stream->push_headers = headp;
1641 }
1642 h = curl_maprintf("%s:%s", name, value);
C1: curl_maprintf("%s:%s", name, value) uses %s format specifiers on name and value pointers without using the available namelen/valuelen parameters
C2: The vulnerable curl_maprintf call is at line 1642 in the on_header function
C4: curl_maprintf("%s:%s", name, value) will read past buffer end via strlen
C5: Vulnerable code path is in PUSH_PROMISE header handling
1643 if(h)
1644 stream->push_headers[stream->push_headers_used++] = h;
1645 return 0;
1646 }
1647
1648 if(stream->bodystarted) {
1649 /* This is a trailer */
1650 CURL_TRC_CF(data_s, cf, "[%d] trailer: %.*s: %.*s",
1651 stream->id, (int)namelen, name, (int)valuelen, value);
1652 result = Curl_dynhds_add(&stream->resp_trailers,
1653 (const char *)name, namelen,
1654 (const char *)value, valuelen);
1655 if(result) {
1656 cf_h2_header_error(cf, data_s, stream, result);
1657 return NGHTTP2_ERR_CALLBACK_FAILURE;
1658 }
1659
1660 return 0;
1661 }
1662
1663 if(namelen == sizeof(HTTP_PSEUDO_STATUS) - 1 &&
1664 memcmp(HTTP_PSEUDO_STATUS, name, namelen) == 0) {
1665 /* nghttp2 guarantees :status is received first and only once. */
1666 char buffer[32];
1667 size_t hlen;
lib/mprintf.clines 1161–1169 · definition of curl_maprintf
1161char *curl_maprintf(const char *format, ...)
1162{
1163 va_list ap_save;
1164 char *s;
1165 va_start(ap_save, format);
1166 s = curl_mvaprintf(format, ap_save);
C1: curl_maprintf calls curl_mvaprintf which processes %s by calling strlen on the pointer
C4: curl_maprintf -> curl_mvaprintf -> strlen for %s format
1167 va_end(ap_save);
1168 return s;
1169}
lib/http2.clines 487–613 · definition of on_header
487static int on_header(nghttp2_session *session, const nghttp2_frame *frame,
C2: on_header function definition starts at line 487
488 const uint8_t *name, size_t namelen,
C1: on_header callback signature shows namelen and valuelen parameters are provided but ignored at line 1642
C3: on_header receives name, namelen, value, valuelen from nghttp2
489 const uint8_t *value, size_t valuelen,
490 uint8_t flags,
491 void *userp);
492#ifndef CURL_DISABLE_VERBOSE_STRINGS
493static int error_callback(nghttp2_session *session, const char *msg,
494 size_t len, void *userp);
495#endif
496static CURLcode cf_h2_ctx_open(struct Curl_cfilter *cf,
497 struct Curl_easy *data)
498{
499 struct cf_h2_ctx *ctx = cf->ctx;
500 struct h2_stream_ctx *stream;
501 CURLcode result = CURLE_OUT_OF_MEMORY;
502 int rc;
503 nghttp2_session_callbacks *cbs = NULL;
504
505 DEBUGASSERT(!ctx->h2);
506 DEBUGASSERT(ctx->initialized);
507
508 rc = nghttp2_session_callbacks_new(&cbs);
509 if(rc) {
510 failf(data, "Could not initialize nghttp2 callbacks");
511 goto out;
512 }
513
514 nghttp2_session_callbacks_set_send_callback(cbs, send_callback);
515 nghttp2_session_callbacks_set_on_frame_recv_callback(cbs, on_frame_recv);
516 nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(cbs,
517 cf_h2_on_invalid_frame_recv);
518#ifndef CURL_DISABLE_VERBOSE_STRINGS
519 nghttp2_session_callbacks_set_on_frame_send_callback(cbs, on_frame_send);
520#endif
521 nghttp2_session_callbacks_set_on_data_chunk_recv_callback(
522 cbs, on_data_chunk_recv);
523 nghttp2_session_callbacks_set_on_stream_close_callback(cbs, on_stream_close);
524 nghttp2_session_callbacks_set_on_begin_headers_callback(
525 cbs, on_begin_headers);
526 nghttp2_session_callbacks_set_on_header_callback(cbs, on_header);
C3: nghttp2_session_callbacks_set_on_header_callback(cbs, on_header) registers on_header as the header callback
C5: on_header callback is active for HTTP/2 sessions
527#ifndef CURL_DISABLE_VERBOSE_STRINGS
528 nghttp2_session_callbacks_set_error_callback(cbs, error_callback);
529#endif
530
531 /* The nghttp2 session is not yet setup, do it */
532 rc = h2_client_new(cf, cbs);
533 if(rc) {
534 failf(data, "Could not initialize nghttp2");
535 goto out;
536 }
537 ctx->max_concurrent_streams = DEFAULT_MAX_CONCURRENT_STREAMS;
538
539 if(ctx->via_h1_upgrade) {
540 /* HTTP/1.1 Upgrade issued. H2 Settings have already been submitted
541 * in the H1 request and we upgrade from there. This stream
542 * is opened implicitly as #1. */
543 uint8_t binsettings[H2_BINSETTINGS_LEN];
544 ssize_t rclen;
545 size_t binlen; /* length of the binsettings data */
546
547 rclen = populate_binsettings(binsettings, data);
548
549 if(!curlx_sztouz(rclen, &binlen) || !binlen) {
550 failf(data, "nghttp2 unexpectedly failed on pack_settings_payload");
551 result = CURLE_FAILED_INIT;
552 goto out;
553 }
554
555 result = http2_data_setup(cf, data, &stream);
556 if(result)
557 goto out;
558 DEBUGASSERT(stream);
559 stream->id = 1;
560 /* queue SETTINGS frame (again) */
561 rc = nghttp2_session_upgrade2(ctx->h2, binsettings, binlen,
562 data->state.httpreq == HTTPREQ_HEAD,
563 NULL);
564 if(rc) {
565 failf(data, "nghttp2_session_upgrade2() failed: %s(%d)",
566 nghttp2_strerror(rc), rc);
567 result = CURLE_HTTP2;
568 goto out;
569 }
570
571 rc = nghttp2_session_set_stream_user_data(ctx->h2, stream->id,
572 data);
573 if(rc) {
574 infof(data, "http/2: failed to set user_data for stream %u",
575 stream->id);
576 DEBUGASSERT(0);
577 }
578 CURL_TRC_CF(data, cf, "created session via Upgrade");
579 }
580 else {
581 nghttp2_settings_entry iv[H2_SETTINGS_IV_LEN];
582 size_t ivlen;
583
584 ivlen = populate_settings(iv, data, ctx);
585 rc = nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE,
586 iv, ivlen);
587 if(rc) {
588 failf(data, "nghttp2_submit_settings() failed: %s(%d)",
589 nghttp2_strerror(rc), rc);
590 result = CURLE_HTTP2;
591 goto out;
592 }
593 }
594
595 rc = nghttp2_session_set_local_window_size(ctx->h2, NGHTTP2_FLAG_NONE, 0,
596 HTTP2_HUGE_WINDOW_SIZE);
597 if(rc) {
598 failf(data, "nghttp2_session_set_local_window_size() failed: %s(%d)",
599 nghttp2_strerror(rc), rc);
600 result = CURLE_HTTP2;
601 goto out;
602 }
603
604 /* all set, traffic will be send on connect */
605 result = CURLE_OK;
606 CURL_TRC_CF(data, cf, "[0] created h2 session%s",
607 ctx->via_h1_upgrade ? " (via h1 upgrade)" : "");
608
609out:
610 if(cbs)
611 nghttp2_session_callbacks_del(cbs);
612 return result;
613}