Skip to content

fix(system): validate the fail-info download name and path #1454 - #1455

Merged
ulleo merged 2 commits into
dataease:mainfrom
borealisalastor-byte:fix/1454-download-fail-info-path
Nov 10, 2026
Merged

ulleo merged 2 commits into
dataease:mainfrom
borealisalastor-byte:fix/1454-download-fail-info-path

Conversation

@borealisalastor-byte

@borealisalastor-byte borealisalastor-byte commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Cause

/system/download-fail-info joined the client-supplied file value onto settings.EXCEL_PATH, checked existence before the suffix, and never verified ownership:

filename = req.file
file_path = os.path.join(path, filename)

if not os.path.exists(file_path):
    raise HTTPException(404, "File Not Exists")
if not filename.endswith('_error.xlsx'):
    raise HTTPException(400, "Only support _error.xlsx")

Two consequences:

  • a name ending with _error.xlsx is opened wherever it points: {"file": "../outside/secret_error.xlsx"} answered 200 with the file outside the upload directory (POSIX; on Windows the ..\..\ and drive-relative forms behave the same);
  • the existence probe ran before the suffix check, so 400 versus 404 disclosed whether an arbitrary path exists.

Fix

  • the name must be a single component: separators, a drive prefix and control characters are refused instead of being silently rewritten (safe_upload_name, shared with the upload-side fix in fix(datasource): store Excel uploads under a sanitised name #1448 #1449);
  • the name shape and the suffix are checked before the filesystem is touched, so a refusal no longer leaks existence;
  • the resolved path must stay inside the upload directory (os.path.realpath + os.path.commonpath, which also resolves symlinks) and must be a regular file; the response keeps returning only the base name.

Tests

backend/tests/test_download_fail_info_path.py mounts the real router on a bare app with the upload directory redirected to a temporary directory and posts real HTTP requests: the normal report is served, a name carrying a directory is not resolved as a path, POSIX / Windows / absolute / drive-relative names never reach the file outside (which stays untouched), a sibling directory sharing the directory prefix is unreachable, an existing file with the wrong suffix and a missing file answer identically, a directory is never downloaded, and a symlink leaving the upload directory is refused.

Verification

  • python -m pytest -q tests/test_download_fail_info_path.py -> 12 passed, 1 skipped (symlink creation is unavailable in the Windows sandbox) after the fix; the same file fails 6 cases against the unmodified source, including the two traversal cases that returned the outside file, run in backend/ on a checkout of main at f10255b;
  • ruff check passes on the changed files;
  • the full uv run pytest -q suite was not run in that sandbox, which has no access to the private xpack index.

Fixes #1454

Summary — control-character follow-up

Commit 217184fe9c6ca56798ae626b47a2bb277f4ba231 preserves the original commit db3ff9b3 and rejects Unicode control characters (C0, DEL and C1) in the original download name before whitespace trimming or filesystem access. The shared upload-name helper is unchanged.

original filename → reject control characters → existing name/suffix validation → path containment → download

Evidence — control-character follow-up

  • Before: the 27 new cases against db3ff9b3 produced 21 failed, 6 passed (python -m pytest -q tests/test_download_fail_info_path.py -k control --tb=no).
  • After: 102 passed for python -m pytest -q tests/test_download_fail_info_path.py tests/test_upload_file_name.py tests/test_excel_http_errors.py, from backend/, covering normal downloads, traversal/symlink rejection and control characters at the beginning, middle and end of names. The new cases verify identical 400 responses for existing/missing report names and no path resolution or file lookup.
  • Tested the follow-up implementation and regression tests before commit; only Ruff formatting followed, with no behavior changes. Reused these results for the delivered commit. ruff check, ruff format --check on both changed files and git diff --check pass.
  • Used the existing Python 3.11 environment without dependency synchronization; confirmed imports resolve to the isolated PR worktree. FastAPI 0.115.14, Starlette 0.46.2, HTTPX 0.28.1, pytest 7.4.4, published sqlbot-xpack 0.0.5.40; PYTHONPATH=. and LOG_FORMAT='%(levelname)s %(message)s'.
  • Scope: offline HTTP router and middleware regressions. Full application authentication, external databases, browser/deployment validation and the full suite were not run. Symlink coverage passed on macOS. No dependency lockfile was changed.

Merge Danger

Door: two-way; code-only validation change, no migration.

Blast Radius: downloads. Requests containing control characters now return 400; normal report names retain the existing behavior.

borealisalastor-byte and others added 2 commits November 9, 2026 18:19


The route joined the client-supplied file name onto EXCEL_PATH, checked existence before the suffix and never verified ownership, so a name ending in _error.xlsx could read outside the upload directory and 400/404 disclosed whether an arbitrary path exists. The name must now be a single component, is checked before touching the filesystem, and the resolved path must stay inside the upload directory.
@ulleo
ulleo merged commit bfc0630 into dataease:main Nov 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] /system/download-fail-info 的 file 参数未做归属校验:可读取上传目录外文件,并可用 404/400 差异探测任意路径

2 participants