feat(linter): route file access through os.Root to prevent path trave…#16
Merged
Conversation
…rsal Upcoming Feature/dependsOn resolution will read files at paths derived from untrusted configuration content (e.g. local Feature references like "./features/foo"). As a proactive defense, LintDir and FindConfigs now perform all filesystem access through a single os.Root opened on the directory being linted, so any path derived from configuration content cannot escape it. Behavior change: a configuration file reached via a symbolic link that escapes the lint directory (including any link with an absolute target) is now treated as nonexistent instead of being followed. Links that resolve within the directory keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
…irectory The os.Root opened by LintDir covers the lint-target directory, which is the parent of .devcontainer, so a symbolic link such as .devcontainer/devcontainer.json -> ../secret.json could still reach unrelated files in the project root. Configuration files under .devcontainer are now discovered and read through a sub-root confined to that directory. The boundary is .devcontainer itself rather than each config's own subfolder, because Features stored inside the active .devcontainer directory can become dependsOn targets once Feature resolution is implemented. Also remove the exported FindConfigs, which lost its last production caller in the os.Root refactoring; discovery is tested directly against the unexported findConfigs instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
Move the os.Root creation out of LintDir and into the CLI: main.go opens each lint path as an os.Root and LintDir only reads files and directories through the root it receives. The not-a-directory check now falls out of os.OpenRoot itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
…r root once Replace findConfigs/readConfig with visitConfigs, which walks the configuration files and hands each one to a callback together with the os.Root it must be read through. The whole walk completes within one call, so the .devcontainer sub-root is opened once and closed by a defer, instead of being reopened by readConfig for every file. The per-file work is extracted into lintConfig, giving each file its own function scope for deferred cleanup. A broken file still does not stop the remaining files from being linted; only a callback error (context cancellation) aborts the visit. The now-unused exported ConfigFile type is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
The feature branch returned fn's error directly, while every other branch used an explicit if-err check because it has work to do afterward. Match the feature branch to that style for consistency. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
…test files linter_test.go relied on the production rules package's no-image-latest rule just to exercise LintDir's file discovery and error handling, which are independent of which rules are registered. Replace it with a package-local test double (like the existing panicRule), removing linter's only dependency on rules from its test binary. Also fold linter_internal_test.go's white-box visitConfigs tests into this file (both now package linter), so the duplicated openRoot helper collapses to one copy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4
nozaq
pushed a commit
that referenced
this pull request
Jul 14, 2026
Resolves the conflict from #16's os.Root-based path traversal hardening in linter.go/linter_test.go: - linter/linter_test.go: PR #16 rewrote this file to use the internal "package linter" test convention (dropping the linter_test/rules imports); the Transform tests added on this branch (flagRule, TestLintTransform) are adapted to that convention, unqualifying references to the now-local package. - cmd/decolint/main.go, linter/linter.go: auto-merged cleanly; runLint's SetTransform wiring for -merge-features is unaffected, since lintPath/LintDir's os.Root confinement only changes how the main devcontainer.json/Feature/Template files are discovered and read, not the *Context passed to Transform. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
nozaq
pushed a commit
that referenced
this pull request
Jul 14, 2026
#16 routed the main devcontainer.json/Feature/Template file discovery through os.Root to prevent path traversal, but local Feature references resolved during -merge-features (a relative path under "features") still went through plain os.ReadFile/os.Stat, bypassing that protection. Thread the same os.Root boundary through: linter.Context gains Dir (the os.Root LintDir already confines the file to) and FileDir (the file's own directory relative to it), populated by lintConfig and left zero when Lint is called directly on in-memory content (as existing tests do). feature.Fetch/Merge now take that (dir, fileDir) pair instead of a bare baseDir string, and resolve local references by joining fileDir with the reference and reading it through dir, so a reference cannot escape the same boundary the referencing devcontainer.json was itself read through. Note this changes local Feature semantics slightly: a Feature stored outside the confining directory (e.g. a project-root sibling of .devcontainer, referenced from .devcontainer/devcontainer.json via "../") no longer resolves, even though it did before #16. This mirrors the boundary #16 already established for the file discovery its own comment anticipated extending to Feature resolution. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UrZdqcow5W3yq4uGMMPS8D
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.
…rsal
Upcoming Feature/dependsOn resolution will read files at paths derived from untrusted configuration content (e.g. local Feature references like "./features/foo"). As a proactive defense, LintDir and FindConfigs now perform all filesystem access through a single os.Root opened on the directory being linted, so any path derived from configuration content cannot escape it.
Behavior change: a configuration file reached via a symbolic link that escapes the lint directory (including any link with an absolute target) is now treated as nonexistent instead of being followed. Links that resolve within the directory keep working.
Claude-Session: https://claude.ai/code/session_012ujRvJhfjXKaXwNVGwgxv4