curl · Report #3973219
57: Heap out-of-bounds read in `curl_easy_escape_ccsid()` / `curl_easy_unescape_ccsid()`
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
- Build an affected curl release, starting with 7.17.0, including the IBM i CCSID compatibility wrappers from
ccsidcurl.c. - Enable AddressSanitizer or an equivalent heap-bounds detector if the target environment supports it.
- Write a small application that calls the public
curl_easy_escape_ccsid()andcurl_easy_unescape_ccsid()wrappers. - Allocate an input buffer containing a few ordinary bytes with no trailing NUL terminator, and pass its exact positive byte length to the wrapper.
- 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.
- Invoke
curl_easy_escape_ccsid()with the bounded, nonterminated input. - Observe that the internal conversion allocates exactly the converted byte count and does not append a NUL because the caller supplied a positive length.
- Confirm that the wrapper discards that converted length and calls
curl_easy_escape()with length zero. - 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. - Repeat the test independently with
curl_easy_unescape_ccsid()and an output-length pointer. - Confirm that the unescape path also passes zero downstream and produces an out-of-bounds read through the URL-decoding path.
- As a control, use a NUL-terminated buffer and verify that the accidental
strlen()dependency no longer crosses the allocation boundary. - 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.
- Confirm a fix by requesting the converted byte count from the conversion helper and forwarding that positive length to
curl_easy_escape()andcurl_easy_unescape(). - Repeat both nonterminated-input tests and verify that each function processes exactly the caller-specified bytes without a sanitizer finding.
- 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.