fix(uploads): reject NUL characters in filenames (#5915)

This commit is contained in:
xbzz1018 2026-09-27 09:29:30 +08:00 committed by GitHub
parent bd168e3552
commit 9abdbc3870
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 33 additions and 0 deletions

View File

@ -65,6 +65,8 @@ def normalize_filename(filename: str) -> str:
safe = Path(filename).name
if not safe or safe in {".", ".."}:
raise ValueError(f"Filename is unsafe: {filename!r}")
if "\x00" in safe:
raise ValueError(f"Filename contains NUL: {filename!r}")
# Reject backslashes — on Linux Path.name keeps them as literal chars,
# but they indicate a Windows-style path that should be stripped or rejected.
if "\\" in safe:

View File

@ -74,6 +74,10 @@ class TestNormalizeFilename:
with pytest.raises(ValueError, match="unsafe"):
normalize_filename(".")
def test_rejects_embedded_nul(self):
with pytest.raises(ValueError, match="NUL"):
normalize_filename("report\x00.pdf")
# ---------------------------------------------------------------------------
# claim_unique_filename

View File

@ -1049,6 +1049,33 @@ def test_upload_files_rejects_dotdot_and_dot_filenames(tmp_path):
assert [f.name for f in thread_uploads_dir.iterdir()] == ["passwd"]
def test_upload_files_skips_nul_filename_without_rolling_back_valid_file(tmp_path):
thread_uploads_dir = tmp_path / "uploads"
thread_uploads_dir.mkdir()
provider = _mounted_provider()
with (
patch.object(uploads, "get_uploads_dir", return_value=thread_uploads_dir),
patch.object(uploads, "ensure_uploads_dir", return_value=thread_uploads_dir),
patch.object(uploads, "get_sandbox_provider", return_value=provider),
):
result = asyncio.run(
call_unwrapped(
uploads.upload_files,
"thread-local",
request=MagicMock(),
files=[
UploadFile(filename="valid.txt", file=BytesIO(b"kept")),
UploadFile(filename="report\x00.pdf", file=BytesIO(b"invalid")),
],
config=SimpleNamespace(),
)
)
assert [file.filename for file in result.files] == ["valid.txt"]
assert (thread_uploads_dir / "valid.txt").read_bytes() == b"kept"
def test_upload_files_rejects_preexisting_symlink_destination(tmp_path):
thread_uploads_dir = tmp_path / "uploads"
thread_uploads_dir.mkdir(parents=True)