Skip to content

fix: prevent path traversal in datasource importToDb (CWE-22) - #1435

Merged
ulleo merged 1 commit into
dataease:mainfrom
jannisd07:fix/importtodb-path-traversal
Nov 2, 2026
Merged

ulleo merged 1 commit into
dataease:mainfrom
jannisd07:fix/importtodb-path-traversal

Conversation

@jannisd07

Copy link
Copy Markdown
Contributor

Cause

POST /datasource/importToDb builds the file path with os.path.join(path, import_req.filePath). filePath comes from the request body. An absolute path replaces the upload directory, and ../ segments lead out of it. The file is then read by read_import_dataframe and imported into a table, so a ws_admin caller can import any file the process can read and pandas can parse (a path ending in .csv, or a workbook). /datasource/parseExcel generates the name on upload, but /importToDb accepted any value.

Fix

Resolve the path with os.path.realpath and require that it lies inside the resolved upload directory (os.path.commonpath), as the Excel upload fixes in #1284 and #1350 do. The upload directory itself ("", ".") is rejected too. Because the path is resolved, a symlink inside the upload directory that points outside is rejected as well.

Rejected requests return 400 with the new i18n_ds_invalid_file_path message, added to all five locales. A path that realpath cannot handle (for example one with a null byte) raises a ValueError; it is caught and gets the same 400. The path check runs before the existence check, so the response does not reveal whether a file exists outside the upload directory. Names returned by /parseExcel keep working.

Tests

New file backend/tests/test_datasource_import_path_traversal.py, offline, in the style of test_excel_http_errors.py (real router and middleware; identity, audit log, session, engine and file reader are stubs). 20 cases:

  • absolute path, ../ variants, .., nested/..: 400
  • sibling directory with the same name prefix (../excel_backup/...): 400
  • missing file outside the upload directory: 400 i18n_ds_invalid_file_path, not File not found
  • symlink to a file or a directory outside: 400
  • "" and ".": 400
  • null byte in filePath: 400
  • missing file inside the upload directory: 400 File not found (unchanged)
  • a file name inside the upload directory: accepted, and the reader gets the resolved path
  • a symlink inside the upload directory that points to a file inside it: accepted
  • upload directory behind a symlink: uploads are still accepted

Without the fix 16 of the 20 cases fail. With the fix, run with Python 3.11.16, sqlbot-xpack 0.0.5.41 and pytest 7.4.4 on base 96f06c7e:

cd backend
uv run pytest -q tests/test_datasource_import_path_traversal.py   # 20 passed
uv run pytest -q                                                   # 411 passed, 83 skipped
uv run ruff check tests/test_datasource_import_path_traversal.py
uv run ruff format --check tests/test_datasource_import_path_traversal.py

BASE_DIR, UPLOAD_DIR, EXCEL_PATH, MCP_IMAGE_PATH, LOCAL_MODEL_PATH and SCRIPT_DIR pointed to a temporary directory because /opt/sqlbot is not writable on the test machine. Of the 83 skipped tests, 77 need an opt-in PostgreSQL database and 6 need the frontend npm dependencies; both groups are existing skips. datasource.py keeps its 18 existing Ruff findings and its existing formatting; no unrelated formatting was applied.

The upload and import flow (/parseExcel with CSV and XLSX files, then /importToDb with the returned filePath) was also checked locally with Chinese names, spaces and several dots in the file name; the frontend sends the filePath from /parseExcel back unchanged.

Not covered

The issue also reports that the import pipeline has no size, row or sheet limit before parsing the workbook in memory. This PR does not change that.

Fixes #1434

@ulleo
ulleo merged commit 3d15c67 into dataease:main Nov 2, 2026
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.

importToDb accepts an arbitrary client-controlled filePath (path traversal / arbitrary file read)

2 participants