fix(security): path containment checks for Windows environments - #3298
fix(security): path containment checks for Windows environments#3298Gracy769 wants to merge 7 commits into
Conversation
- Fixed an issue in extract_path_candidates where shlex.split(posix=True) would strip backslashes from Windows paths, mangling UNC paths (e.g. \\server\share) before they could be evaluated by _is_windows_absolute. - Fixed a bypass in validate_path where Windows absolute paths bypassed glob expansion and symlink resolution. On Windows, they now fall through to the standard Path logic, allowing glob expansion and strict resolution while still properly checking containment.
|
Confirmed real issue: shlex.split(posix=True) indeed strips backslashes and mangles UNC paths on Windows. The fall-through for validate_path on Windows to standard Path logic is the right call, good fix. One suggestion: adding a test that feeds a UNC path (e.g. \server\share\foo) through extract_path_candidates and validate_path would prevent regression, since this path-normalization bug is easy to reintroduce. |
|
Nice — adding the UNC path regression test (2477cf2) covers exactly the scenario I was worried about, so this fix now has proper guardrails against reintroduction. The Windows path fall-through to standard Path logic is solid. Looks ready to merge once checks pass. |
|
Confirmed both bugs, and the second one is the more serious of the two. On the On the Worth adding a regression test that drops a symlink escaping the allowed root and asserts it's rejected — that's the case most likely to silently regress if the fast-path is ever reintroduced. |
|
Thanks for adding One thing worth calling out: the Other than that this looks good to me — the |
…k and add mocked escape test
|
Addressed the unprivileged Windows runner feedback in commit 38f8cfa:
All 12 security test cases pass cleanly. |
|
Good — the junction fallback is the right fix for the skip problem. A skipped assertion is worse than a failing one, because it reports green on the exact case the test exists to catch, so getting test_issue_3007_symlink_escape_is_denied and test_windows_absolute_symlink_escape_is_denied down to 0 skips is what actually makes the containment check trustworthy on non-elevated runners. One boundary worth flagging: NTFS junctions can only target local directories and cannot point at UNC/remote paths, so the _create_directory_link fallback can't cover a link that resolves to a network location. That remains the variant most likely to slip through if a fast-path is ever reintroduced. test_symlink_resolution_escape_mocked covers the logic deterministically, but a short comment in the helper noting the junction limitation would make the gap discoverable to whoever touches this next. Remaining blocker on my side: Checks is still at 0 — one workflow is awaiting maintainer approval, so CI hasn't actually run against 38f8cfa. Once that's approved and green, this is good to merge. |
There was a problem hiding this comment.
🟡 Changes recommended
src/path_scope.py introduces a whitespace-only line (trailing whitespace) that will fail git diff --check and should be cleaned up before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR tightens the Python port’s workspace path containment logic for Windows-style paths and adds regression tests to cover Windows symlink/junction escapes and UNC path tokenization.
Changes:
- Adjust
WorkspacePathScope.validate_path()so Windows absolute paths on Windows go through the standard glob +Path.resolve()containment logic (instead of the PureWindowsPath-only branch). - Update
extract_path_candidates()to prioritize raw token splitting so UNC paths (e.g.\\server\share\...) aren’t mangled byshlex.split(posix=True). - Expand the security scope test suite to cover Windows symlink/junction scenarios and mocked resolution escapes (including UNC).
File summaries
| File | Description |
|---|---|
| tests/test_security_scope.py | Adds Windows-focused regression tests (junction fallback, absolute-path symlink escape, UNC preservation, mocked resolve escapes). |
| src/path_scope.py | Updates Windows absolute path handling and candidate extraction ordering to prevent Windows/UNC path validation bypasses. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| elif not any(_is_windows_absolute(str(root)) for root in self.roots): | ||
| # Even on Windows, deny if no roots are Windows absolute paths (edge case) | ||
| return PathScopeDecision(False, 'windows absolute path is outside workspace scope', str(candidate), raw) | ||
|
|
|
2b271e1 clears the trailing-whitespace line Copilot flagged, so d5efc4b addresses the other item I raised: documenting the NTFS junction limitation in the helper plus the mocked UNC link case means the junction-can't-target-UNC gap is now discoverable in-tree instead of living in this thread. From my side the code is settled. The remaining gate is unchanged: one workflow is still awaiting maintainer approval, so Checks is at 0 and 38f8cfa / 2b271e1 have not actually run in CI. The junction fallback and the 0-skip symlink assertions are exactly the kind of thing that only proves itself on a real Windows runner, so I'd hold merge until that workflow is approved and green rather than merging on review alone. |
|
Following up on the junction limitation I raised earlier: the other edge worth pinning down is the failure mode of resolve(strict=True). For a path that does not exist yet, or a dangling link, resolve(strict=True) raises OSError/FileNotFoundError instead of returning something to compare, so the containment decision then depends entirely on how that exception is handled. That should be an explicit deny rather than a fall back to the lexical check, otherwise a non-resolvable path effectively skips the resolution step this PR just added, which is the same bypass in a different shape. A test asserting that a dangling or unresolvable path is rejected rather than passed through or crashing would make that guarantee explicit. |
Summary
Anti-slop triage
Verification
git diff --checkpasses.Resolution gate