Fall back to a safe name when upload filename sanitization yields a traversal token

Merged
MA
2mo ago
+72-7

All Submissions:

  • Have you followed the guidelines stated in CONTRIBUTING.md file?
  • Have you checked to ensure there aren't any other open Pull Requests for the desired changed?

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Changes To Core Features:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your core changes, as applicable?
  • Have you successfully ran tests with your changes locally?

Defect

FINDING-007 (SECURITY, MEDIUM) from 0.9.9a1 pre-release testing: _sanitize_upload_filename in packages/reflex-components-core/src/reflex_components_core/core/_upload.py (introduced by #6753) returns the bare traversal token .. for filenames composed entirely of dot/traversal segments ("..", "./../.", "..\\", "/.."). With no safe segment left it falls through to PureWindowsPath(filename).name, which is .. for those inputs. Handlers using the canonical get_upload_dir() / file.name then point at the upload directory's parent, and the write raises an unhandled IsADirectoryError inside the ndjson streaming generator, producing an HTTP 500 — despite the docstring promising a safe relative path. Confirmed live against the buffered upload endpoint; the streamed path shares the same sanitizer.

Fix

Guard the sanitizer's final result: anything that reduces to "", ".", or ".." now falls back to the bare name upload, which stays inside the upload directory. The guard covers both the absolute/drive-path branch ("/..", "C:\\..") and the relative branch where no safe segment survives. Both buffered and streamed uploads go through this one function, so both paths are fixed. Behavior for everything else is unchanged: normal hostile names ("../../evil.txt" -> evil.txt, "a b<>|.txt" kept), relative directories, and legitimate dot-like names ("...", "....", unicode dot lookalikes) all sanitize exactly as before.

Test plan

  • New regression tests in tests/units/components/core/test_upload.py:
    • test_upload_filename_sanitization_traversal_only_falls_back — parametrized over "..", "./../.", "..\\", "/..", "/foo/..", "C:\\..", "..//..", "", "."; all fail against unfixed main (sanitizer returned .. or "") and pass with the fix.
    • test_buffered_upload_traversal_only_filename_falls_back — buffered path via _upload_file_from_starlette, asserts path == Path("upload") and name == "upload".
    • test_chunk_parser_traversal_only_filename_falls_back — streamed path via the chunk multipart parser, asserts the emitted chunk filename is upload and data intact.
    • test_upload_filename_sanitization_keeps_dot_like_names — "...", "....", and unicode-dot variants unchanged; extended the existing drops-path-segments parametrization with "../../evil.txt" and "a b<>|.txt" to lock in existing behavior.
  • uv run pytest tests/units/components/core/test_upload.py — 56 passed.
  • uv run ruff check . / uv run ruff format . — clean.
  • uv run pyright reflex tests — 0 errors.
  • News fragment: packages/reflex-components-core/news/+upload-sanitizer-dots.bugfix.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x


Generated by Claude Code