Repository navigation
Python: fix(python): reject dot-segment remote paths in SessionsPythonTool.upload_file - #14563
Conversation
…load_file _construct_remote_file_path only prefixed /mnt/data/ and let "." and ".." through, so a model-supplied remote_file_path such as "../../escape.txt" was transmitted to the sessions service as /mnt/data/../../escape.txt. download_file rejects exactly these segments (added in microsoft#14520), so the two directions of the same plugin disagreed on the same input. Apply the same guard on the upload path, in the same helper, with the same exception type.
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The focused validation preserves ordinary path handling, has regression coverage, and has no identified blocking issues.
Review effort: Balanced
Findings: None
What changed in this PR
Adds remote-path validation to Python session uploads, matching the existing download restriction on literal . and .. path segments.
Changes:
- Reject dot segments in the remote-path helper and document the constraint.
- Add regression tests for rejected paths and preserved ordinary paths.
| File | Description |
|---|---|
| python/tests/unit/core_plugins/test_sessions_python_plugin.py | Tests rejected dot segments and accepted ordinary paths. |
| python/semantic_kernel/core_plugins/sessions_python_tool/sessions_python_plugin.py | Adds and documents remote-path validation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Community review. I am not a maintainer, so this does not clear the merge gate.
I read the full diff and the surrounding sessions_python_plugin.py (upload_file, _construct_remote_file_path, and the existing check in download_file), then ran the tests and a probe on the PR head (python/ in a venv, Python 3.13, no network).
What it does: _construct_remote_file_path now rejects any /-separated segment equal to . or .. with a FunctionExecutionException, which is the same check download_file already does on remote_file_name (line 448). _construct_remote_file_path is only called from upload_file (line 366), so the change affects only uploads. The check runs on the raw argument before the /mnt/data/ prefix is added, and before any auth or network call.
What I ran:
tests/unit/core_plugins/test_sessions_python_plugin.pyon the PR head: 86 passed. Withsemantic_kernel/taken from origin/main, the 9 newtest_upload_file_rejects_dot_segments[...]cases fail and the rest pass. The three "ordinary path" cases intest_upload_file_allows_ordinary_remote_pathspass on both, so they act as a guard and not as a bug repro.- A direct probe of
_construct_remote_file_path:a/../b,/mnt/data/../x,a/./b,./andx/..are rejected..hidden,...,a/.../b,dir/anda//bare accepted and come out under/mnt/data/unchanged.
Observations, none of them blocking:
- The check is on literal segments only.
%2e%2e/banda/%2e%2e/bpass through unchanged (/mnt/data/%2e%2e/b). Indownload_filethe name is URL-quoted, so that string would be double-encoded there. For upload the value is sent as the multipartfilename(files = {"file": (remote_file_path, ...)}), and I did not check how the sessions service treats an encoded name, so I do not know whether it matters. A leading space (../b) also passes. I only mention these so you know where the line is drawn. - The new test docstring says upload must reject the same segments as download. The two error messages differ slightly ("remote file path" versus "remote file name"), which is fine, and the tests match on "path segments" only.
I did not run the integration tests or CI, and I did not try the real Azure sessions service.
I have no blocking concerns: the change does what it says, it matches the existing download check, and the new tests fail without it.
Motivation and Context
download_filerejects.and..path segments in the remote file name (#14520). The upload direction of the same plugin never got the equivalent guard:_construct_remote_file_pathonly prefixed/mnt/data/and passed the rest through, so a model-suppliedremote_file_pathof../../escape.txtwas transmitted to the sessions service as/mnt/data/../../escape.txt.The two directions of one plugin therefore disagreed on the same input, and the permissive one is reachable straight from a tool call:
remote_file_pathis a string supplied by the model, not a path object, so nothing between the tool argument and the request body rejects the segments. Local validation cannot help here, which is why #13478'sallowed_upload_directoriescheck does not cover this case.Description
Applies the guard in the same helper
download_fileuses, with the same exception type and the same split on/, so the check cannot drift from the download side again:Placing it in
_construct_remote_file_pathrather than inline inupload_filemeans any future caller is covered by default.Testing
test_sessions_python_plugin.py; no new file.".","..","../x","../../x","./x","nested/./x","nested/../x","nested/..","/mnt/data/../../x") fail withDID NOT RAISEwhen the guard is reverted, and pass with it in place..hidden.txtanda..b/c.txt, since neither is a standalone.or..segment.ruff check,ruff format --checkandmypyare clean on both files.Note on the local test run: the errors seen in this file's fixtures are reproducible on unmodified
mainin my environment (a local dependency gap), so they are not caused by this change.One thing I did not change
SessionsPythonPlugin.UploadFileAsyncon the .NET side has the same gap - it only doesVerify.NotNullOrWhiteSpace(remoteFileName). I kept this PR to the Python side to stay reviewable and single-purpose, since your recent work on this plugin has paired .NET and Python (e.g. #14085, #14140). Happy to extend it if you would rather land both together.🤖 Generated with Claude Code