HexBrain

curl · Report #4024268

Out-of-bounds read in curl_formadd when CURLFORM_NAMELENGTH is explicitly 0

Informativestatus
7upvotes
15 Sept 2026disclosed

How it was found

Hi all,

I've discovered a heap out-of-bounds read in curl_formadd that causes libcurl to silently read past a caller-supplied buffer and transmit the leaked contents to the remote server. It's confirmed under ASan and reachable without violating any documented precondition.

Impact

An application that passes a dynamically-computed length of 0 alongside a non-null-terminated buffer to curl_formadd will have libcurl read an unbounded number of bytes past the end of that allocation and include the overread data in the form submission. The practical scenario is an application that constructs multipart form fields from user input or file content, where an empty field name (zero bytes) is a valid state. In that case the application may legitimately pass CURLFORM_NAMELENGTH, 0L alongside a freshly allocated buffer that hasn't been null-terminated.

Root cause

The problem is in forminfo_copyfield (lib/formdata.c:121):

static CURLcode forminfo_copyfield(struct bufref *field, size_t len)
{
  const char *value = Curl_bufref_ptr(field);
  CURLcode result = CURLE_OK;

  if(value) {
    if(!len)
      len = strlen(value);           /* line 128 */
    result = Curl_bufref_memdup0(field, value, len);
  }

  return result;
}

The intent of the if(!len) branch is to handle the case where the caller didn't supply a length at all, so libcurl should derive it from the string. The problem is that struct FormInfo stores namelength as a plain size_t, zero-initialized, and there's no separate flag indicating whether the caller actually provided a value. Zero means both "not set" and "explicitly set to zero".

When formadd_check runs (lib/formdata.c:264) it passes form->namelength directly:

if(!(form->flags & HTTPPOST_PTRNAME) &&
   forminfo_copyfield(&form->name, form->namelength))
  return CURL_FORMADD_MEMORY;

If the caller passed CURLFORM_NAMELENGTH, 0L, form->namelength is 0, and forminfo_copyfield falls through to strlen(value). If value points to a buffer that was never null-terminated (say, a malloc(8) whose bytes are all 0x41), strlen walks past the end of the allocation until it finds a null byte. Whatever it reads gets duplicated into the form part and sent to the server.

Note also that the CURLFORM_NAMELENGTH handler in formadd (lib/formdata.c:380) treats a nonzero stored value as "already set" for the duplicate-option check:

if(curr->namelength)
    retval = CURL_FORMADD_OPTION_TWICE;
else
    curr->namelength = (size_t)form_int_arg(long);

So passing CURLFORM_NAMELENGTH, 0L stores 0, skips the duplicate check, and leaves no trace that the option was explicitly provided.

PoC

#include <curl/curl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
  struct curl_httppost *post = NULL;
  struct curl_httppost *last = NULL;
  char *name = malloc(8);
  CURLFORMcode rc;

  if(!name)
    return 1;
  memset(name, 'A', 8);

  curl_global_init(CURL_GLOBAL_ALL);
  rc = curl_formadd(&post, &last,
                    CURLFORM_COPYNAME, name,
                    CURLFORM_NAMELENGTH, 0L,
                    CURLFORM_COPYCONTENTS, "value",
                    CURLFORM_END);
  printf("curl_formadd returned %d\n", (int)rc);
  curl_formfree(post);
  curl_global_cleanup();
  free(name);
  return 0;
}

...

=================================================================
==71006==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000838 at pc 0x000105efe93c bp 0x00016b9054f0 sp 0x00016b904cb0
READ of size 9 at 0x602000000838 thread T0
    #0 0x000105efe938 in strlen+0x1b4 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x7a938)
    #1 0x000104673f54 in forminfo_copyfield formdata.c:128
    #2 0x0001046734bc in formadd_check formdata.c:264
    #3 0x00010466eb58 in formadd formdata.c:565
    #4 0x00010466c4c0 in curl_formadd formdata.c:610
    #5 0x0001044f8f68 in main poc_formadd_namelen_zero.c:18
    #6 0x0001853b6b94  (<unknown module>)

0x602000000838 is located 0 bytes after 8-byte region [0x602000000830,0x602000000838)
allocated by thread T0 here:
    #0 0x000105ec1330 in malloc+0x78 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x3d330)
    #1 0x0001044f8e28 in main poc_formadd_namelen_zero.c:10
    #2 0x0001853b6b94  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow formdata.c:128 in forminfo_copyfield
Shadow bytes around the buggy address:
  0x602000000580: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x602000000600: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x602000000680: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x602000000700: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
  0x602000000780: fa fa fd fd fa fa fd fd fa fa fd fd fa fa 00 fa
=>0x602000000800: fa fa 00 fa fa fa 00[fa]fa fa 00 00 fa fa 05 fa
  0x602000000880: fa fa 00 00 fa fa 00 00 fa fa 00 00 fa fa 00 00
  0x602000000900: fa fa 00 00 fa fa 00 fa fa fa 00 fa fa fa 00 00
  0x602000000980: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
  0x602000000a00: fa fa fd fd fa fa fd fa fa fa fd fd fa fa fd fd
  0x602000000a80: fa fa fd fd fa fa fd fd fa fa fd fd fa fa fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==71006==ABORTING

Fix direction

Add a set of boolean flags to struct FormInfo to record whether each length field was explicitly provided by the caller (namelength_set, contentslength_set, bufferlength_set). Set the flag in the formadd switch when the option is seen, regardless of the value. In forminfo_copyfield (or in formadd_check before the call), only fall back to strlen when the flag is absent. When the flag is set and the length is 0, pass 0 directly and skip the strlen entirely. That's a zero-byte copy, which is the caller's intent.

An alternative is to use a sentinel (e.g., SIZE_MAX meaning "not set") and store the real value including 0 directly, but the boolean-flag approach is less likely to interact badly with the existing CURL_FORMADD_OPTION_TWICE checks.

Cheers, Josh

Impact

OOB read

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original