Skip to content

test_safe_join_rejects_symlink_escape fails on Windows without elevation or Developer Mode #3408

Description

@iam-kira

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:

tests\shared\test_path_security.py:145: in test_safe_join_rejects_symlink_escape
    (sandbox / "escape").symlink_to(outside)
E   OSError: [WinError 1314] A required privilege is not held by the client

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 pytest
returns 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:

try:
    (sandbox / "escape").symlink_to(outside)
except OSError as exc:
    pytest.skip(f"symlink creation is not permitted here: {exc}")

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

git clone https://github.com/modelcontextprotocol/python-sdk
cd python-sdk
uv sync
uv run pytest tests/shared/test_path_security.py

Python & MCP Python SDK

Python 3.14.7 | Windows-11-10.0.26200-SP0
mcp 2.0.1.dev23+56af447
mcp-types 2.0.1.dev23+56af447
pytest 8.4.2

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    on Aug 28, 2026
  2. hhhhzzzj commented on Aug 30, 2026

    @hhhhzzzj

    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 unconditional symlink_to() at line 145 raises OSError [WinError 1314] before safe_join() is reached, so the test fails during setup rather than checking the escape case.

    The smallest fix looks like keeping the existing PathEscapeError assertion and wrapping only symlink_to() in try/except OSError, calling pytest.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 in src/mcp/shared/path_security.py:121-178 does not need to change.

    One detail I would leave to your preference: should the skip cover any OSError from 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.

  3. iam-kira commented on Aug 30, 2026

    @iam-kira
    Author

    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:

    1. 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.

    2. 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 by suppress(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.

  4. tonydzi commented on Sep 1, 2026

    @tonydzi
  5. maxisbey commented on Oct 2, 2026

    @maxisbey
    Contributor

    This is fixed on main by #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.

    AI Disclaimer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions