OOB read / info leak in convert.* stream filters when line-break-chars contains NUL

Kurzinfo

Metadaten

  • Original (extern): Link oeffnen
  • Veroeffentlicht: 2026-09-24 10:07 UTC
  • Importiert: 2026-10-21 14:15 UTC
  • CVSS: 5.9
  • Quelle-ID: php_sec
  • uid_hash: 94ebb163253045dd994ed519711bbe6295025bfc0732bd5d94dc4dec1adbe56a

Inhalt

Summary

The convert.base64-encode, convert.quoted-printable-encode and convert.quoted-printable-decode stream filters accept a line-break-chars option whose length is tracked separately from the string itself. The filter constructors duplicate the value with pestrdup(), which stops at the first NUL byte, while keeping the original length. When the filter later emits a line break it copies the recorded length out of the truncated allocation, reading past its end and placing adjacent heap bytes into the filter output.

Details

php_conv_base64_encode_ctor() duplicates the option with pestrdup() but stores the untruncated length:

https://github.com/php/php-src/blob/php-8.5.10/ext/standard/filters.c#L237-L238

php_conv_qprint_encode_ctor() and php_conv_qprint_decode_ctor() repeat the same pattern at lines 838 and 1051. The over-read happens when the encoder inserts a line break and copies lbchars_len bytes from that allocation:

https://github.com/php/php-src/blob/php-8.5.10/ext/standard/filters.c#L406

With line-break-chars set to "\0X" the duplicate is a one-byte allocation holding only the NUL terminator, while lbchars_len is still 2, so one byte past the allocation is copied into the output. Extending the option after the NUL increases the number of bytes read out of bounds.

The fix replaces pestrdup() with pestrndup(lbchars, lbchars_len, persistent) in all three constructors, so the allocation always matches the recorded length.

PoC

USE_ZEND_ALLOC=0 ASAN_OPTIONS=detect_leaks=0 ./sapi/cli/php -n -r '
$opts = ["line-length" => 4, "line-break-chars" => "\0X"];
$fp = fopen("php://temp", "r+");
fwrite($fp, str_repeat("A", 6));
rewind($fp);
stream_filter_append($fp, "convert.base64-encode", STREAM_FILTER_READ, $opts);
echo bin2hex(stream_get_contents($fp)), "\n";
'

The line break in the output should be the two bytes 0058. Instead the second byte comes from adjacent memory, and AddressSanitizer reports:

==1271431==ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 2 at 0x7bb9d15e2731 thread T0
0x7bb9d15e2731 is located 0 bytes after 1-byte region [0x7bb9d15e2730,0x7bb9d15e2731)
    #1 php_conv_base64_encode_convert ext/standard/filters.c:406
    #2 strfilter_convert_append_bucket ext/standard/filters.c
    #3 strfilter_convert_filter ext/standard/filters.c:1513

The same leak is reachable over HTTP wherever an application forwards request data into the filter options:

 4,
    "line-break-chars" => base64_decode($_GET["lb64"]), // e.g. "\0X"
];
$fp = fopen("php://temp", "r+");
fwrite($fp, str_repeat("A", 6));
rewind($fp);
stream_filter_append($fp, "convert.base64-encode", STREAM_FILTER_READ, $opts);
echo bin2hex(stream_get_contents($fp));

Impact

Heap memory adjacent to the line-break-chars allocation is copied into the filter output, so an application that returns the encoded result discloses those bytes to the requester. The attacker can enlarge the leak by lengthening the option value after the NUL byte. A crash is possible on some allocators and builds, but the practical risk is the information leak.

Exploitation requires the application to pass attacker-controlled bytes as line-break-chars and for those bytes to contain an embedded NUL, which is rare in practice, so the majority of applications are not affected.

Verknuepfte CVEs

CVE-ID Severity (Quelle) CVSS (Quelle) Severity (CVE.org) CVSS (CVE.org) EPSS EPSS-Pctl Veroeffentlicht (CVE.org)

CVE-2026-92842

MEDIUM

5.9

MEDIUM

5.9

2026-09-25

Quellen-Details

Bezeichnung Name Kategorie Tags Zielgruppe Sprache Feed-URL
PHP Security (php/php-src GHSA)

php_sec

vendor_advisory php, runtime - de https://github.com/php/php-src/security/advisories