Upload fails with HTTP 500 for filenames in Greek, Arabic, Hebrew, CJK and other scripts
What happened
I tried to attach documents to a task and some of them failed with a "Server error" in the UI. At first it looked like a problem with one particular file, but it turned out to depend only on which characters the filename contains.
It is not tied to one language. A Greek name like Πρόγραμμα_έργου.txt, an Arabic
one like محادثة_المشروع.txt, a Hebrew one like נספח_לפרויקט.txt, a Chinese one
like 中文报告.txt, an accented Latin one like café_à_í.txt and a name with an emoji all
fail the same way. Other names in the very same scripts upload fine, which is what
made this confusing at first.
When I reloaded the task the attachment was actually there, but its name had been truncated, so the upload half-succeeded.
Environment
- Leantime 3.9.8 (
leantime/leantime:3.9.8, officialdocker-leantimecompose stack) - PHP 8.3.32, MySQL 8.4, local disk storage
Steps to reproduce
- Log in and open any task.
- Create files with these names and attach them:
for n in "Πρόγραμμα_έργου.txt" "محادثة_المشروع.txt" "נספח_לפרויקט.txt" \ "中文报告.txt" "café_à_í.txt" "_план.txt" "Отчёт_Работа.txt"; do echo "test" > "$n" done
Result
Each of those returns 500, while a control name from the same script returns 200:
| Script | Filename | Result |
|---|---|---|
| Greek | Πρόγραμμα_έργου.txt |
500 |
| Greek | Ελληνικά.txt |
200 |
| Arabic | محادثة_المشروع.txt |
500 |
| Arabic | العربية.txt |
200 |
| Hebrew | נספח_לפרויקט.txt |
500 |
| Hebrew | עברית.txt |
200 |
| CJK | 中文报告.txt |
500 |
| CJK | 日本語.txt |
200 |
| Emoji | _план.txt |
500 |
| Latin-1 | café_à_í.txt |
500 |
| Cyrillic | Отчёт_Работа.txt |
500 |
| ASCII | plain.txt |
200 |
Each 500 adds one entry to the log.
What I found in the logs
storage/logs/leantime-YYYY-MM-DD.log:
production.ERROR: Malformed UTF-8 characters, possibly incorrectly encoded
InvalidArgumentException ... Illuminate/Http/JsonResponse.php:90
#0 symfony/http-foundation/JsonResponse.php(49): Illuminate\Http\JsonResponse->setData(Array)
#3 app/Domain/Files/Controllers/Upload.php(56): Illuminate\Routing\ResponseFactory->json(Array)The row is created despite the 500, with a truncated name:
id module encName extension realName
1 ticket d09fe271a550fbe3e6ae78081b132ca3 docx Test_Cause
sanitizeFilename() uses preg_replace() with the x modifier but without u.
Without u, PCRE matches bytes rather than code points, so the class
[\x7F\xA0\xAD] matches the raw bytes 0xA0 and 0xAD. In UTF-8 those are
continuation bytes, not characters. Replacing one with - splits a multi-byte
sequence and produces invalid UTF-8, which JsonResponse::setData() then rejects.
That is why the failure follows individual characters rather than languages: any
character whose UTF-8 encoding happens to contain 0xA0 or 0xAD triggers it.
0x7F cannot appear inside a multi-byte sequence, so it is harmless here.
| Script | Characters that break, out of the block | Examples |
|---|---|---|
| Greek U+0370..U+03FF | 4 of 144 | Π έ Ϡ ϭ |
| Arabic U+0600..U+06FF | 8 of 256 | ح ٠ ڭ ؠ |
| Hebrew U+0590..U+05FF | 4 of 112 | נ ֠ ֭ |
| CJK U+4E00..U+4FFF | 16 of 512 | 中 丠 你 传 |
| Hiragana U+3040..U+309F | 2 of 96 | だ ね |
| Emoji U+1F300..U+1F5FF | 24 of 768 | |
| Latin-1 Supplement | 4 of 96 | à í |
| Latin Extended-A | 4 of 128 | Ġ ĭ Š ŭ |
| Cyrillic U+0400..U+04FF | 8 of 256 | Р Э Ѡ Ҡ |
The file is written to disk and inserted into zp_file before the response is
serialized, which is why the attachment exists even though the request returned 500.
Two copies of the pattern are affected, and a grep over app/ shows they are the
only occurrences:
app/Core/Files/FileManager.php:55app/Core/Support/String/SanitizeFilename.php:29
$filename = preg_replace(
'~
[<>:"/\\|?*]|
[\x00-\x1F]|
[\x7F\xA0\xAD]|
[#\[\]@!$&\'()+,;=]|
[{}^\~`]
~x',
'-', $filename);SanitizeFilename is also used by Core/Support/Avatarcreator.php for the
initials-avatar cache filename, so user names go through the same code. A user named
Πέτρος Παπάς hits it through their initials.
Proposed fix
Add the u modifier in both places:
- ~x',
+ ~xu',With u, \xA0 and \xAD mean the code points U+00A0 (NBSP) and U+00AD
(SOFT HYPHEN), which is what the comment next to them describes.
I applied this locally and re-ran the same set. Every name that returned 500 now
returns 200, the controls are unchanged, and no new Malformed UTF-8 entries appear:
| Script | Filename | Before | After |
|---|---|---|---|
| Greek | Πρόγραμμα_έργου.txt |
500 | 200 |
| Arabic | محادثة_المشروع.txt |
500 | 200 |
| Hebrew | נספח_לפרויקט.txt |
500 | 200 |
| CJK | 中文报告.txt |
500 | 200 |
| Emoji | _план.txt |
500 | 200 |
| Latin-1 | café_à_í.txt |
500 | 200 |
| Cyrillic | Отчёт_Работа.txt |
500 | 200 |
| controls | Ελληνικά.txt, العربية.txt, עברית.txt, 日本語.txt, plain.txt |
200 | 200 |
All twelve names are stored intact and pass mb_check_encoding(..., 'UTF-8').
Initials avatars for Πέτρος Παπάς and Эдуард Романов render and cache correctly
as user-ππ.svg and user-эр.svg.
Note when testing against a running container: the image ships
opcache.revalidate_freq = 60, so an edited PHP file is not picked up for up to a
minute. Restart the container between the before and after runs, otherwise both runs
measure the same bytecode.
Related
This looks like the same defect as #3075 ("FileName been replace by '_'", Chinese filenames), which was closed as fixed on 2026-01-22.
The wholesale replacement reported there is indeed gone. v3.5.9 used
preg_replace('/[^a-zA-Z0-9_.-]/', '_', $filename), and from v3.6.0 onward the
current pattern is in place. But the replacement pattern never got the u
modifier, so a subset of the same filenames still fails, now with a 500 instead of
underscores.
The reporter's own observation there fits this exactly: they noted that 上海.docx
uploads fine while 中文测试.docx fails. 中 is U+4E2D, encoded as E4 B8 AD, so it
contains 0xAD; neither 上 nor 海 contains 0xA0 or 0xAD.
I can still reproduce this on v3.9.8, which is the current release, and both
app/Core/Files/FileManager.php and
app/Core/Support/String/SanitizeFilename.php on master still read ~x', today.
Source: Leantime/leantime