Skip to content

[13.x] Fix multibyte case-insensitive matching in Str::replace and Str::remove - #60882

Merged
taylorotwell merged 7 commits into
laravel:13.xfrom
marekmiklusek:fix/str-replace-case-insensitive-multibyte
Aug 2, 2026
Merged

[13.x] Fix multibyte case-insensitive matching in Str::replace and Str::remove#60882
taylorotwell merged 7 commits into
laravel:13.xfrom
marekmiklusek:fix/str-replace-case-insensitive-multibyte

Conversation

@marekmiklusek

Copy link
Copy Markdown
Contributor

Str::replace() and Str::remove() with $caseSensitive = false delegate
to str_ireplace(), which only case-folds ASCII characters. Multibyte
characters are never matched case-insensitively:

Str::replace('ž', 'X', 'Žltý kôň', false);     // "Žltý kôň" — nothing replaced
Str::replace('KÔŇ', 'pes', 'žltý kôň', false); // "žltý kôň" — nothing replaced
Str::remove('ž', 'Žltý', false);               // "Žltý" — nothing removed

This is inconsistent within the class itself -> Str::contains() considers
the same strings a case-insensitive match:

Str::contains('Žltý', 'ž', ignoreCase: true); // true

ASCII-only search terms keep using str_ireplace() exactly as before.
Multibyte terms are matched via preg_quote + /iu patterns with a callback
replacement, replicating str_ireplace() array pairing and sequential
semantics. Subjects that are not valid UTF-8 fall back to str_ireplace(),
preserving the previous behavior.

Comment thread src/Illuminate/Support/Str.php Outdated
Comment thread src/Illuminate/Support/Str.php Outdated
@taylorotwell

Copy link
Copy Markdown
Member

Agent review

Blocking

  • src/Illuminate/Support/Str.php:1326 — An invalid UTF-8 search term such as "\xFF" reaches the /u pattern, producing a warning and returning null instead of preserving str_ireplace() behavior.
  • src/Illuminate/Support/Str.php:1325 — An earlier replacement can introduce invalid UTF-8, causing a subsequent replacement to return null; for example, replacing ['ž', 'é'] with ["\xFF", 'x'] in 'ŽÉ'.

Should Fix

  • src/Illuminate/Support/Str.php:1304 — Selecting PCRE based on the combined search terms changes ASCII-term behavior in mixed arrays, so adding 'ž' makes an existing 's' term unexpectedly match Unicode 'ſ', contrary to the stated compatibility goal.
  • src/Illuminate/Support/Str.php:1326 — PCRE /iu uses different case-folding semantics from Str::contains()’s mb_strtolower(), so replacements match pairs such as 'ſ'/'S' and 'Σ'/'ς' that contains(..., ignoreCase: true) rejects.

Consider

  • tests/Support/SupportStrTest.php:949 — Add malformed UTF-8 and mixed ASCII/multibyte search cases to cover the fallback and compatibility guarantees described by the PR.

@taylorotwell
taylorotwell marked this pull request as draft July 26, 2026 13:24
@marekmiklusek
marekmiklusek force-pushed the fix/str-replace-case-insensitive-multibyte branch from 316494e to da659f2 Compare July 27, 2026 15:07
@marekmiklusek

marekmiklusek commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, all addressed:

  • Invalid UTF-8 (both blocking points): all inputs (search terms,
    replacements, subject) are now validated upfront; if anything is not valid
    UTF-8, the whole call falls back to str_ireplace(), preserving previous
    behavior exactly. Covered by adversarial fuzzing with malformed bytes in
    every position (0 nulls, byte-identical to native).

  • Mixed arrays: PCRE is now selected per term, not per call, ASCII terms
    always use str_ireplace(), so adding a multibyte term no longer changes
    their semantics (test added with ſ).

  • Case-folding vs contains(): implementing replacement via
    mb_strtolower() folding isn't sound, some folds change string length
    (İ), so folded offsets can't be mapped back to the original for
    splicing. PCRE simple folding only applies to genuinely multibyte terms
    now, and for the divergent pairs (ſ/S, ς/Σ) it matches a superset
    of what contains() accepts. Happy to adjust if you'd prefer different
    semantics here.

@marekmiklusek
marekmiklusek marked this pull request as ready for review July 27, 2026 15:14
Comment thread src/Illuminate/Support/Str.php Outdated
marekmiklusek and others added 3 commits July 27, 2026 19:33
Co-authored-by: Sebastian Hädrich <11225821+shaedrich@users.noreply.github.com>
@taylorotwell
taylorotwell merged commit c03ba3f into laravel:13.x Aug 2, 2026
50 of 53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants