Repository navigation
Extend path_within_root containment guard to the component, directive, and view goto-definition flows - #192
Merged
mikebronner merged 2 commits intoJun 16, 2026
Conversation
…to-definition. Extend the #130 slot-navigation containment guard to the remaining filesystem-touching goto-definition flows. A loadViewsFrom-style namespace can resolve an absolute path that escapes the project root; these flows previously handed the LSP client a LocationLink pointing outside the root with only a file_exists_cached check. Add a path_within_root(&path, &config.root) guard inside each candidate loop, after the existence check and before building the link, with continue on failure (mirrors the slot-navigation pattern): - create_view_location_from_salsa - resolve_component_existing_file, over the class-backed and PSR-4 candidates component_candidate_paths appends past resolve_component_path's own root filter - create_directive_location_from_salsa, every candidate loop (@component x2, view-first-arg, @includeWhen, @includeFirst, @livewire) Cover each flow with out-of-root, in-root, and under-root-symlink containment tests in the slot_navigation_containment.rs style. Fixes: #148
mikebronner
marked this pull request as ready for review
June 16, 2026 16:46
There was a problem hiding this comment.
✅ Approved
Review Summary
- Reviewed PR #192 — extends the
path_within_root(&path, &config.root)containment guard to the view, component, and directive goto-definition flows inlaravel-lsp/src/main.rs, mirroring the slot-navigation guard from #130/#143. - All 10 acceptance criteria met. Guards sit in every candidate loop after
file_exists_cachedand before theLocationLink/return, withcontinue(notreturn None) so a later in-root candidate can still resolve:create_view_location_from_salsa(main.rs:13821) ✅resolve_component_existing_file(main.rs:13871) ✅create_directive_location_from_salsa— all six loops:@componentboth try-blocks (14048,14060), view-directives-first-arg (14077),@includeWhen(14094),@includeFirst(14111),@livewire(14134) ✅- Every guard reads
config.rootfrom the in-scopeget_cached_config(); the pre-existingself.root_pathread (13856) feeds onlyComposerAutoloadand is not introduced as a new guard read — AC4 satisfied as written. - The
@featurebranch is correctly exempt — it builds its path fromroot.join(..)and is already contained (comment at14021).
- Tests are honest and discriminating. All three new modules create the out-of-root target on disk before asserting
None, so each test would fail if the guard were removed — not no-ops. Positive (in-root) cases assert the exacttarget_uri(not justis_some()), and each flow has a Unix-gated symlink-under-root→outside-target case that only thecanonicalize-basedpath_within_rootcan reject. Real functions are exercised viaLspService::new+inner()— no stubs. All three modules registered intests/mod.rs. - Security:
path_within_rootcanonicalizes both sides and uses component-wisestarts_with— symlink-safe and not foolable by an adjacent-directory prefix. The escape vector this PR targets is closed across all three flows. - CI green — LSP test/fmt/clippy, extension wasm/fmt/clippy, and CodeQL all pass.
📋 Non-blocking follow-ups
- The
<livewire:ns::component>tag flow is still unguarded —create_livewire_location_from_salsa(main.rs:13981), reached viaPatternAtPosition::Livewire(~20689), is not one of the loops this PR touched (that's the@livewiredirective, which is guarded at14134). It returns aLocationLinkafter onlyfile_exists_cached, with nopath_within_root. SinceLivewireConfig.component_namespacesaccepts bare absolute paths (livewire_config.rs:293), a<livewire:x::passwd>tag could hand the client an out-of-root navigation target — the same namespace-escape class this PR fixes elsewhere. Navigation-target escape only (defense-in-depth), not an active server-side read — the same tier the issue body assigns to the view/component/directive flows. The next sibling in the #130 → #148 chain; tracking as a follow-up, not blocking this PR. - (secondary, lower value) Diagnostic-validation loops at
main.rs:15925and16603call.exists()onresolve_view_pathresults with no containment check, so an out-of-root namespace causesstatprobes outside the root. No client leak and no file read — folding into the same follow-up for tracking.
Ready for @mikebronner to merge.
6 tasks
mikebronner
deleted the
fix/148-extend-path-within-root-guard-to-component-directive-view
branch
June 16, 2026 17:45
11 tasks done
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #148
Extends the slot-navigation
path_within_rootcontainment guard from #130 (PR #143) to the remaining filesystem-touching goto-definition flows: view, component, and directive. AloadViewsFrom(__DIR__ . '/../../etc', 'ns')-style namespace can resolve an absolute path that escapes the project root; before this change these flows handed the LSP client aLocationLinkpointing outside the root with only afile_exists_cachedcheck. This is hardening / defense-in-depth — these flows surface a navigation target rather than reading out-of-root file content — bringing the containment invariant to every FS-touching goto-definition path.Changes
create_view_location_from_salsa: guard inside the candidate loop, afterfile_exists_cached, before building theLocationLink,continueon failure.resolve_component_existing_file: guard afterfile_exists_cached, beforereturn Some(path).resolve_component_pathalready filters its candidates against the root, butcomponent_candidate_pathsappends class-backed (Blade::component('tag', Class::class)) and PSR-4componentNamespacecandidates past that filter — the guard covers those.create_directive_location_from_salsa: guard inside every candidate loop — the two@componenttry-blocks, the view-directives-first-arg loop (@extends/@include/@includeIf/@includeUnless/@each), the@includeWhenloop, the@includeFirstloop, and the@livewireloop. The@featurebranch builds its path fromroot.join(..)and is already contained, so it is left as-is.config.rootfrom the in-scopeget_cached_config()result, consistent with the slot-navigation pattern; no newself.root_pathread is introduced.tests/mod.rs.Acceptance Criteria
path_within_rootguard increate_view_location_from_salsacandidate loop (after existence check, beforeLocationLink,continueon failure)path_within_rootguard inresolve_component_existing_file(after existence check, beforereturn Some(path),continue)path_within_rootguard in every candidate loop ofcreate_directive_location_from_salsa(@component both-try blocks, view-first-arg,@includeWhen,@includeFirst,@livewire)config.root;self.root_pathnot introduced as an additional readview_navigation_containment.rs): out-of-root →None, in-root →Some, under-root symlink →None(Unix)component_navigation_containment.rs): out-of-root →None, in-root →Some(resolver +create_component_location_from_salsa), under-root symlink →Nonedirective_navigation_containment.rs):@include(view directive) and@livewireout-of-root →None, in-root →Some, under-root symlink →Nonetests/mod.rsslot_navigation_containment,folio_cursor_containment,slot_variable_resolutiontests remain greencargo fmt --checkclean,cargo clippy --all-targetscleanTest Plan
cargo fmt --check— cleancargo clippy --all-targets— clean (no lints)cargo testlib (1843) + bin (339) targets — all green, including the 20 containment tests (10 new: view×3, directive×4, component×3)Fixes #148