HexBrain

curl · Report #3973219

57: Heap out-of-bounds read in `curl_easy_escape_ccsid()` / `curl_easy_unescape_ccsid()`

Informativestatus
5upvotes
7 Sept 2026disclosed

How it was found

Summary

curl_easy_escape_ccsid() and curl_easy_unescape_ccsid() in projects/OS400/ccsidcurl.c discard the converted byte count returned by dynconvert() and pass 0 to the downstream curl_easy_escape() / curl_easy_unescape(). When the caller supplies a positive explicit length with non-NUL-terminated input, dynconvert() does not append a NUL and may shrink the allocation to exactly the converted size. The downstream 0 triggers strlen(), which reads past the allocation boundary.

Present from curl-7_17_0 through at least curl-8_21_0. Tested at commit c2676bf9e639e31e44b12a2cc82cf2046231b056.

Root cause

In projects/OS400/ccsidcurl.c, dynconvert() only appends a NUL when slen < 0 (lines 180-193). With positive slen, it converts exactly that many bytes, then reallocs down to the exact converted count (line 196).

Both wrappers discard this length:

// curl_easy_escape_ccsid — line 283
s = dynconvert(ASCII_CCSID, string, length ? length : -1, sccsid, NULL);
// ...
d = curl_easy_escape(handle, s, 0);   // line 288 — 0 means strlen()

// curl_easy_unescape_ccsid — line 312
s = dynconvert(ASCII_CCSID, string, length ? length : -1, sccsid, NULL);
// ...
d = curl_easy_unescape(handle, s, 0, outlength);  // line 317

curl_easy_escape() at lib/escape.c:59 and Curl_urldecode() at lib/escape.c:115 both resolve 0 via strlen(), scanning past the tight allocation into adjacent heap memory.

Affected versions:

IBM i/OS400-specific wrappers in curl 7.17.0 through 8.21.0, plus affected 8.22.0 prerelease/development versions.

Reproduction

  1. Build an affected curl release, starting with 7.17.0, including the IBM i CCSID compatibility wrappers from ccsidcurl.c.
  2. Enable AddressSanitizer or an equivalent heap-bounds detector if the target environment supports it.
  3. Write a small application that calls the public curl_easy_escape_ccsid() and curl_easy_unescape_ccsid() wrappers.
  4. Allocate an input buffer containing a few ordinary bytes with no trailing NUL terminator, and pass its exact positive byte length to the wrapper.
  5. Use matching source and destination CCSIDs, such as CCSID 819 to CCSID 819, to exercise the deterministic copy path without making the result depend on a complex character conversion.
  6. Invoke curl_easy_escape_ccsid() with the bounded, nonterminated input.
  7. Observe that the internal conversion allocates exactly the converted byte count and does not append a NUL because the caller supplied a positive length.
  8. Confirm that the wrapper discards that converted length and calls curl_easy_escape() with length zero.
  9. Observe the vulnerable result: the zero length causes strlen() to scan beyond the converted heap allocation, and the memory checker reports a heap out-of-bounds read.
  10. Repeat the test independently with curl_easy_unescape_ccsid() and an output-length pointer.
  11. Confirm that the unescape path also passes zero downstream and produces an out-of-bounds read through the URL-decoding path.
  12. As a control, use a NUL-terminated buffer and verify that the accidental strlen() dependency no longer crosses the allocation boundary.
  13. If native IBM i instrumentation is unavailable, compile the actual IBM i wrapper source in a sanitizer-enabled environment with only the minimum platform declarations needed for compilation. Keep the tested path on the same-CCSID copy branch so platform conversion stubs are not executed.
  14. Confirm a fix by requesting the converted byte count from the conversion helper and forwarding that positive length to curl_easy_escape() and curl_easy_unescape().
  15. Repeat both nonterminated-input tests and verify that each function processes exactly the caller-specified bytes without a sanitizer finding.
  16. Separately test embedded-NUL input to ensure the final repair consistently preserves the documented bounded-input semantics across both conversion stages.

Expected result

When the caller supplies a positive length, both CCSID wrappers must process exactly that many bytes without requiring or searching for a NUL terminator.

Vulnerable result

The wrappers discard the known converted length and pass zero to the underlying APIs. Zero requests strlen()-based length discovery, causing a read beyond the exact-sized nonterminated conversion buffer.

Suggested fix

Request the converted length through dynconvert()'s olen parameter and forward it:

--- a/projects/OS400/ccsidcurl.c
+++ b/projects/OS400/ccsidcurl.c
@@ -273,6 +273,7 @@
 {
   char *s;
   char *d;
+  int converted_length;

   if(!string) {
@@ -280,12 +281,13 @@
     return (char *)NULL;
   }

-  s = dynconvert(ASCII_CCSID, string, length ? length : -1, sccsid, NULL);
+  s = dynconvert(ASCII_CCSID, string, length ? length : -1, sccsid,
+                 &converted_length);

   if(!s)
     return (char *)NULL;

-  d = curl_easy_escape(handle, s, 0);
+  d = curl_easy_escape(handle, s, converted_length);
   free(s);

Same change for curl_easy_unescape_ccsid() (pass converted_length instead of 0).

Impact

Triggering requires the IBM i CCSID API, a positive explicit input length, and non-NUL-terminated converted output. The consequence is a heap out-of-bounds read: the escape path may return percent-encoded bytes from adjacent heap memory; the unescape path may process bytes beyond the intended boundary. I have not reproduced the overread natively on IBM i or identified a remote application path through these wrappers. I would consider Low the maximum plausible severity given the platform-specific API, application-controlled input, and undemonstrated native impact.

Impact

Summary:

An application using the IBM i CCSID escape wrappers with attacker-influenced bounded input may read beyond the converted heap allocation. This can crash the process or cause adjacent heap bytes to be incorporated into escaped output, potentially disclosing process memory; however, native IBM i disclosure and a concrete remote application path have not been demonstrated.

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