Fall back to a safe name when upload filename sanitization yields a traversal token - #6971
Merged
Conversation
…raversal token
_sanitize_upload_filename returned the bare traversal token ".." for
filenames composed entirely of dot/traversal segments (e.g. "..",
"./../.", "..\\", "/.."): with no safe segment left it fell through to
PureWindowsPath(filename).name, which is ".." for those inputs. Handlers
building get_upload_dir() / file.name then pointed at the upload dir's
parent and crashed with an unhandled IsADirectoryError -> HTTP 500,
despite the docstring promising a safe relative path.
Guard the final result: anything that sanitizes to "", "." or ".."
now falls back to the bare name "upload", which stays inside the upload
directory. Both the buffered and streamed upload paths share the
sanitizer, so both are covered. Legitimate dot-like names ("...",
unicode dot lookalikes) and normal hostile names ("../../evil.txt")
keep their existing behavior.
Found as FINDING-007 during 0.9.9a1 pre-release testing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
Merging this PR will not alter performance
Comparing Footnotes
|
Contributor
Greptile SummaryThis PR hardens upload filename sanitization by falling back to
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| packages/reflex-components-core/src/reflex_components_core/core/_upload.py | Adds a safe fallback for filenames that reduce to unusable dot-and-space-only segments. |
| tests/units/components/core/test_upload.py | Adds sanitizer and end-to-end regression coverage for buffered and streamed uploads. |
| packages/reflex-components-core/news/+upload-sanitizer-dots.bugfix.md | Records the upload filename sanitization fix. |
Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile
Contributor
There was a problem hiding this comment.
All reported issues were addressed across 3 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Win32 strips trailing dots and spaces when it opens a path, so a segment
like ".. " navigates to the parent directory there even though it is not
literally "..". Filter segments on `part.strip(". ")` instead of an exact
token match so the fallback covers those variants in both the absolute/drive
and relative branches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWNuEGFrVZ5g9zMCzb3eU1
masenf
pushed a commit
that referenced
this pull request
Aug 28, 2026
A ~29s 1920x1080 motion graphic covering the end-user-facing changes in 0.9.9, sourced from the 0.9.9a1 and 0.9.9a2 changelog entries across reflex, reflex-base, the component packages and reflex-hosting-cli. Nine beats: the dev-mode main-thread win (#6905), call-site auto-memoization of @rx.memo (#6949), the move to React Router 8 (#6854), DevTools component naming (#6945), stdlib logging with --json plus the client_error report (#6863, #6865, #6827), the cloud CLI's whoami/token and autoscaling flags (#6918, #6884, #6948), and a grid of the smaller fixes a user would notice (#6786, #6593, #6944, #6971, #6790, #6920). Styled to match reflex.dev by reusing its design system rather than approximating it: the dark violet/slate scales, wave-line motif, radial bloom and type scale come from reflex-site-shared, and the fonts are the same @fontsource-variable Instrument Sans and JetBrains Mono files the site serves. Source only — the rendered mp4 is a build artifact, regenerated by `npm run build` and ignored. Lives outside the packaged tree (hatch only includes /reflex), so it does not affect the published wheel or sdist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016arkVKmoogzUtZNfZZSvSo
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All Submissions:
Type of change
Changes To Core Features:
Defect
FINDING-007 (SECURITY, MEDIUM) from 0.9.9a1 pre-release testing:
_sanitize_upload_filenameinpackages/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 toPureWindowsPath(filename).name, which is..for those inputs. Handlers using the canonicalget_upload_dir() / file.namethen point at the upload directory's parent, and the write raises an unhandledIsADirectoryErrorinside 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 nameupload, 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
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, assertspath == Path("upload")andname == "upload".test_chunk_parser_traversal_only_filename_falls_back— streamed path via the chunk multipart parser, asserts the emitted chunk filename isuploadand 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.packages/reflex-components-core/news/+upload-sanitizer-dots.bugfix.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01EMjBXPozsNeQNSBZecNH8x
Generated by Claude Code