Skip to content

ext/mbstring: mb_regex_substitute() reads past the replacement string's end on a truncated trailing multibyte lead byte #24769

Closed as not planned

Description

@kunwaratulhax0r

Description

509e27bae1 (GH-23499, fixed August 2026) corrected two places in mb_regex_substitute() (ext/mbstring/php_mbregex.c, the replacement-string interpolation behind mb_ereg_replace()/mb_eregi_replace()) where php_mb_mbchar_bytes()'s claimed character width was used to advance a pointer without checking it against the buffer's actual remaining length -- both were clamped to MIN(p + clen, eos). f6852befa3 (GH-24498, fixed October 2026) separately fixed the same bug class in a different file, php_mb_rfc1867_getword().

Neither fix touched the first php_mb_mbchar_bytes() call in mb_regex_substitute()'s main loop -- the check that runs on every character, before any backslash/backref handling, to decide whether it's an ordinary character or the start of a backreference:

while (p < eos) {
	...
	clen = (int) php_mb_mbchar_bytes(p, enc);
	if (clen != 1 || p == eos || p[0] != '\\') {
		/* skip anything that's not an ascii backslash */
		smart_str_appendl(pbuf, p, clen);
		p += clen;
		continue;
	}

If the character at p is not a backslash, and its lead byte (looked up in the current mbregex encoding's mblen_table) claims a width of 2-4 bytes but fewer than clen - 1 bytes actually remain before eos, smart_str_appendl(pbuf, p, clen) copies clen bytes starting at p regardless -- reading past the replacement string's real content. This is the identical shape as the two already-fixed call sites a few lines later in the same function, which use MIN(p + clen, eos) instead.

Minimal reproducer:

<?php
mb_regex_encoding('UTF-8');
$replacement = "x" . "\xF0"; // ends in a UTF-8 4-byte lead byte with 0 continuation bytes present
var_dump(mb_ereg_replace('a', $replacement, 'a'));

Resulted in this output:

string(5) "x..."

(5 bytes: the input is only 2 bytes, but php_mb_mbchar_bytes() claims the trailing \xF0 byte is a 4-byte character, so 4 bytes get copied starting from it -- 3 of them past the replacement string's actual end.)

But I expected this output instead: a 2-byte result ("x\xF0"), i.e. the same MIN(p + clen, eos)-style clamping already applied to the other two call sites in this function.

This was repeatable across 20 differently-sized/content inputs ending the same way -- always exactly claimed_width - 1 extra bytes beyond the logical input length. In shorter variants the extra bytes were non-zero, unpredictable content rather than a fixed pattern, consistent with genuine adjacent heap memory being copied into the return value.

Suggested fix: apply the same clamp already used nearby in the same function:

clen = (int) php_mb_mbchar_bytes(p, enc);
if (clen != 1 || p == eos || p[0] != '\\') {
	size_t avail = MIN((size_t) clen, (size_t) (eos - p));
	smart_str_appendl(pbuf, p, avail);
	p += avail;
	continue;
}

Confirmed present, unmodified, on PHP-8.3, PHP-8.4, and PHP-8.5, and master.

PHP Version

PHP 8.7.0-dev (cli) (built: Oct 26 2026 09:57:01) (NTS DEBUG)
Copyright © The PHP Group and Contributors
Zend Engine v4.7.0-dev, Copyright © Zend by Perforce
    with Zend OPcache v8.7.0-dev, Copyright ©, by Zend by Perforce

Also confirmed present on the PHP-8.3, PHP-8.4, and PHP-8.5 branches via direct content check.

Operating System

Ubuntu 24.04 (Docker), but this is pure string-handling logic, not OS-specific.

Activity

  1. kunwaratulhax0r commented on Oct 27, 2026

    @kunwaratulhax0r
    Author

    Hi @lazerg appreciate the fast turnaround on this issue. Quick side note: I have also raised two security vulnerabilities. Could you help point someone in the right direction to review them?

  2. lazerg commented on Oct 27, 2026

    @lazerg
    Contributor

    Security reports go through the private form at https://github.com/php/php-src/security/advisories/new, or to security@php.net if the form doesn't work for you. See SECURITY.md. If you filed yours as regular issues, send them there instead, with a short reproducer.

  3. kunwaratulhax0r commented on Oct 27, 2026

    @kunwaratulhax0r
    Author

    Yeah already reported via private form https://github.com/php/php-src/security/advisories/new. Will wait for 2 more days to get a confirmation. Thanks for the quick response. Much appreciated.

  4. iluuu1994 commented on Oct 28, 2026

    @iluuu1994
    Member

    Please don't report potential security issues publicly...

    Edit: Oh, I see Ilia already did that.

  5. kunwaratulhax0r commented on Oct 28, 2026

    @kunwaratulhax0r
    Author

    Hi @iluuu1994 I believe above reported issue is general issue not a potential security issue. If you think so I can move it to privately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions