#3782·leantime

Upload fails with HTTP 500 for filenames in Greek, Arabic, Hebrew, CJK and other scripts

Author: loberaPogCreated Sep 11, 2026Updated Sep 11, 2026

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, official docker-leantime compose stack)
  • PHP 8.3.32, MySQL 8.4, local disk storage

Steps to reproduce

  1. Log in and open any task.
  2. Create files with these names and attach them:
    bash
    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:55
  • app/Core/Support/String/SanitizeFilename.php:29
php
$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:

diff
-        ~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.