Repository navigation
test_safe_join_rejects_symlink_escape fails on Windows without elevation or Developer Mode #3408
Description
Activity
I reproduced this on the current main revision (d2290ca) in a Windows environment without symlink privileges. In
tests/shared/test_path_security.py:140-148, the unconditionalsymlink_to()at line 145 raisesOSError [WinError 1314]beforesafe_join()is reached, so the test fails during setup rather than checking the escape case.The smallest fix looks like keeping the existing
PathEscapeErrorassertion and wrapping onlysymlink_to()in try/exceptOSError, callingpytest.skip()when the platform does not permit symlink creation. That keeps the security assertion active on runners with symlink support and gives ordinary Windows checkouts a clean baseline. The runtime implementation insrc/mcp/shared/path_security.py:121-178does not need to change.One detail I would leave to your preference: should the skip cover any
OSErrorfrom symlink creation, or only the privilege/unsupported-symlink cases? If this one-file test scope is right, I can prepare the focused PR after the issue is assigned.Thanks for reproducing it independently — good to have it confirmed at a
specific revision, and your read of the scope matches mine: test-only, and
src/mcp/shared/path_security.py doesn't need to change.On your question — catch broad
OSError, not the specific privilege cases.
Two reasons:-
WinError 1314 surfaces as a plain
OSError. Python maps some Windows error
codes onto subclasses (FileExistsError, PermissionError), but 1314 isn't in
that table, so there is no narrow type to catch. You would have to match on
.winerror, which is Windows-only and brittle. -
Narrow catching is exactly how this bug class survives. Test suite is unrunnable on Windows without symlink privilege: utils_temp_dir errors pypa/pipx#2021 is
the same failure caused bysuppress(FileExistsError)— it looks like a
guard, and WinError 1314 walks straight through it. Their entire test suite
errors on Windows as a result.
The precedent I'd point at is psf/black, which uses
except (OSError, NotImplementedError)for its symlink tests — NotImplementedError
covers platforms where os.symlink exists but is unimplemented.So:
try: (sandbox / "escape").symlink_to(outside) except OSError as exc: pytest.skip(f"symlink creation is not permitted here: {exc}")I have this written and verified locally — the suite goes from 5791 passed /
1 failed to 5790 passed / 0 failed on Windows, and the test skips for the right
reason rather than passing vacuously. Happy to open the PR once a maintainer
assigns it, or to leave it to you if you'd rather take it.-
This is fixed on
mainby #3609. Thanks for the clear write-up and the offer of a PR.It's a little different from the skip you suggested. When
symlink_to()fails with WinError 1314, the test falls back to a directory junction, which needs no privilege, so the escape assertion still runs on machines like yours instead of being skipped.I had no unprivileged Windows machine to try that path on, so if it still fails for you, or you hit a different error, please open a new issue.
Initial Checks
Release line
2.x (current stable)
Description
Running the test suite on Windows as a normal, non-elevated user fails in
tests/shared/test_path_security.py:
The test creates a symlink unconditionally. Windows only permits symlink creation for an
elevated process, or for a normal user with Developer Mode enabled — neither is the
default state of a Windows machine.
Why CI doesn't catch it: the Windows job in .github/workflows/shared.yml is green, and
this test only passes when symlink_to() succeeds, so those runners evidently do have the
privilege. The failure appears only on an ordinary developer machine, so the suite is
permanently green in CI and permanently red locally.
Why it matters: it denies a Windows contributor a clean baseline. A first
uv run pytestreturns a failure unrelated to their change, and the natural assumption — that their own
setup is broken — costs time. Everything else passes: 5791 passed, 1 failed, 16 skipped.
It also weakens the suite's signal, since someone who learns to expect one red test may
not notice a second.
Suggested fix — skip when the platform refuses, leaving every assertion intact so the
test still exercises safe_join wherever symlinks work, including all current CI:
This is the only test in the suite that creates a symlink. I have this prepared and
verified locally (suite goes to 0 failures; the test skips for the right reason rather
than passing vacuously). Happy to open a PR — following CONTRIBUTING, I'll wait for the
issue to be assigned first.
Example Code
Python & MCP Python SDK