fix(policy): resolve project_root before path-traversal check#895
Conversation
|
@microsoft-github-policy-service agree |
APM Review Panel VerdictDisposition: APPROVE (with one required pre-merge action: CHANGELOG entry) Per-persona findingsPython Architect: This is a routine bug fix (one new private helper, one early-resolve call). The module is purely procedural; the class diagram below shows module boundary and function roles. OO / class diagram classDiagram
direction TD
class path_security {
<<Module>>
+validate_path_segments(path_str, context, reject_empty, allow_current_dir)
+ensure_path_within(path, base_dir) Path
+safe_rmtree(path, base_dir)
-_strip_extended_prefix(p) Path
}
class PathTraversalError {
<<Exception>>
}
class discovery_get_cache_dir {
<<CallerFunction>>
+_get_cache_dir(project_root) Path
}
path_security ..> PathTraversalError : raises
discovery_get_cache_dir ..> path_security : calls ensure_path_within
note for path_security "_strip_extended_prefix: new private helper\nensure_path_within: containment guard\nboth modified in this PR"
note for discovery_get_cache_dir "project_root = project_root.resolve()\nadded early (#886)"
class path_security:::touched
class discovery_get_cache_dir:::touched
classDef touched fill:#fff3b0,stroke:#d47600
Execution flow diagram flowchart TD
A["_get_cache_dir(project_root)"] --> B["[FS] project_root = project_root.resolve() -- NEW #886"]
B --> C["base = project_root / apm_modules"]
C --> D["candidate = base / POLICY_CACHE_DIR"]
D --> E["ensure_path_within(candidate, project_root)"]
E --> F["[FS] resolved = path.resolve()"]
E --> G["[FS] resolved_base = base_dir.resolve()"]
F --> H["_strip_extended_prefix(resolved) -- NEW #886"]
G --> I["_strip_extended_prefix(resolved_base) -- NEW #886"]
H --> J["resolved.is_relative_to(resolved_base)"]
I --> J
J -->|False| K["raise PathTraversalError"]
J -->|True| L["return resolved -- prefix-stripped on Windows"]
L --> M["return candidate"]
Design patterns
Structural note (follow-up, not blocker): CLI Logging Expert: No changes to CLI output, DevX UX Expert: No CLI surface changes. The Windows path normalization fix is transparent to users -- no behavioral change is visible at the command surface. Supply Chain Security Expert:
No security regression. Containment is preserved. Implementation is correct. Auth Expert: Not activated -- the changed files ( OSS Growth Hacker: Internal infrastructure fix -- no direct conversion surface (README, quickstart) affected. Windows compatibility reliability is a quiet enterprise trust signal for Windows-heavy teams. No story angle strong enough to stand alone; if two or more Windows fixes cluster in the same release, worth batching under a "works everywhere" release note. Side-channel to CEO: tagging this in the next release narrative compounds the "Windows reliability" positioning advantage at low cost. No CEO arbitrationAll specialists agree -- no disagreements to arbitrate. The change is a narrow, well-targeted fix for a Windows-specific path comparison failure caused by inconsistent The one gap is structural, not behavioral: the PR carries no Required actions before merge
Optional follow-ups
|
Description
_get_cache_dirpasses an unresolvedproject_roottoensure_path_within, which then resolvescandidateandbase_dirindependently. On Windows,Path.resolve()on an existing path expands 8.3 short names (e.g.RUNNER~1->runneradmin) and may add the\?\extended-length prefix, but on a not-yet-existing path it keeps the original form. Whencandidatedoesn't exist yet (first writer in a concurrent scenario), the two sides resolve to different forms andis_relative_tofails withPathTraversalError.Two changes:
discovery.py: resolveproject_rootonce at the top of_get_cache_dirso the candidate path inherits the long-name formpath_security.py: strip the\?\extended-length prefix inensure_path_withinbefore comparison, since Windowsresolve()adds it inconsistentlyFixes #886
Type of change
Testing
test_unresolved_project_root_does_not_raise) that passes a symlink-indirect project root through_get_cache_dir