fix(packages): validate manager name against allowlist in restore/diff#85
Merged
Merged
Conversation
The <manager> positional argument in `nodeup packages restore` and
`nodeup packages diff` was interpolated verbatim into a snapshot
filename via `fmt.Sprintf("%s-%s.json", managerName, version)`, then
`filepath.Join`'d into <DataDir>/snapshots. Because `filepath.Join`
collapses `..` segments, a manager name like `../../tmp/evil`
resolved outside the snapshots directory.
Worse, the read snapshot's `Packages` list is piped straight into
`npm install -g <name>@<version>` with no validation, so an attacker
with a local file-placement primitive (a shared temp dir, a cloned
repo containing a payload filename like
`<prefix>../../tmp/evil-1.0.0.json`) could redirect the migration to
install arbitrary packages onto the user's machine.
Close the confused-deputy surface by validating `managerName` against
a canonical allowlist before any file path is constructed:
- New `detector.AllowedManagerNames()` derives the allowlist from
`detector.All()` so the set stays in sync with per-platform
registry_*.go build files (e.g. nvm-windows only on Windows).
- New `detector.IsAllowedManagerName(name)` does a byte-for-byte
case-sensitive lookup — matching the `--manager` flag's lookup
convention so a future regression that flips the case-fold
surfaces immediately.
- `runRestore` (positional branch) and `runDiff` both bail with
the offender plus the full allowlist in the error message, so a
typo stays user-fixable without grepping docs.
- New tests cover traversal payloads (`../../tmp/evil`,
`../etc/passwd`, `/etc/passwd`, `..\\..\\evil`, `./fnm`),
case-fold negativity (`FNM`, `FnM`), and near-miss strings
(`fnmm`, `fn`, ` fnm`, `fnm/`), plus an end-to-end cobra
`cmd.Execute()` test pinning the runRestore/runDiff error message.
Refs #51
|
✔️ 45eba73 - Conventional commits check succeeded. |
dipto0321
pushed a commit
that referenced
this pull request
Jul 5, 2026
…pkg-doc collision + dead FileOverlay Residual stale-comment / dead-code cleanups from the 2026-07-03 re-review that didn't belong in any of the active fix-wave PRs. All deletions / docstring edits — no behavior change. - internal/detector/fnm.go: delete ErrFNMNotImplemented (zero call sites, no tests, mutations have been real since PR #56); rewrite the FNM struct doc to describe the actual implemented surface (detection + mutations + layout queries) instead of "Phase 1 only, mutations not implemented"; drop the "left as stubs in Phase 1" paragraph from the mutation section. - internal/packages/sentinel.go: demote `// Package sentinel implements…` to `// Sentinel file handling: …` — godoc attributes package-doc comments to the actual package name (`packages`), so the rendered godoc was wrong. Verified no canonical `// Package packages …` comment exists elsewhere in the package, so this was the sole package-level doc and the move doesn't leave the package undocumented. - internal/config/resolve.go: delete superseded FileOverlay() (the coarse all-fields overlay that marked explicit-zero values as set — exactly the bug FileOverlayFromNode was introduced to fix in #85). Update the Overlay type's doc comment to point at FileOverlayFromNode as the file-layer builder. Closes #104. Co-Authored-By: puku-ai-2.8 <noreply@puku.sh>
dipto0321
pushed a commit
that referenced
this pull request
Jul 5, 2026
Residual stale-comment / dead-code cleanups from the 2026-07-03 re-review that didn't belong in any of the active fix-wave PRs. All deletions / docstring edits — no behavior change. - internal/detector/fnm.go: delete ErrFNMNotImplemented (zero call sites, no tests, mutations real since PR #56); rewrite the FNM struct doc to describe the actual surface (detection + mutations + layout queries) instead of "Phase 1 only, not implemented"; drop the "left as stubs in Phase 1" paragraph from the mutation section. - internal/packages/sentinel.go: demote "// Package sentinel implements..." to "// Sentinel file handling: ..." — godoc attributes package-doc comments to the actual package name (`packages`), so the rendered godoc was wrong. Verified no canonical "// Package packages ..." comment exists elsewhere, so the package isn't left undocumented. - internal/config/resolve.go: delete superseded FileOverlay() (the coarse all-fields overlay that marked explicit-zero values as set — exactly the bug FileOverlayFromNode was introduced to fix in #85). Update the Overlay type's doc comment to point at FileOverlayFromNode as the file-layer builder. Closes #104. Co-Authored-By: puku-ai-2.8 <noreply@puku.sh>
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
nodeup packages restore <manager> <version>andnodeup packages diff <manager> …interpolated the user-supplied<manager>slot straight into a snapshot filename (fmt.Sprintf("%s-%s.json", managerName, version)) and thenfilepath.Join'd it into<DataDir>/snapshots. Becausefilepath.Joincollapses..segments, a manager name like../../tmp/evilresolved outside the snapshots directory.That's a confused-deputy surface: the read snapshot's
Packageslist is fed straight intonpm install -g <name>@<version>with no validation, so an attacker with a local file-placement primitive (a shared temp directory, a cloned repo with a payload filename like<prefix>../../tmp/evil-1.0.0.json) could redirect the migration to install arbitrary packages onto the user's machine.This PR validates
<manager>against a canonical allowlist (detector.IsAllowedManagerName) before any filesystem path is constructed. The allowlist is derived fromdetector.All(), so it stays in sync with the per-platformregistry_*.gobuild files (e.g. nvm-windows only appears on Windows).Linked issues
Fixes #51
Type of change
fix— bug fix (PATCH bump)feat— new feature (MINOR bump)refactor— no behavior changeperf— performance improvementdocs— documentation onlytest— tests onlychore— maintenance / dependency bumpci— CI/CD onlybuild— build system onlyChecklist
feat(scope): subject)make cilocally and it passesdocs/, inline godoc)Screenshots / output
Negative-path before/after on
nodeup packages restore:Negative-path on
nodeup packages diff:Positive-path stays unchanged:
Files touched
internal/detector/manager_names.go— new file.AllowedManagerNames()(derived fromAll()) andIsAllowedManagerName(name)(case-sensitive lookup). Doc comments explain the security rationale and reference fix(packages): path traversal via unsanitized manager name in packages restore/diff #51.internal/detector/manager_names_test.go— new file.TestAllowedManagerNames_MatchesAllpinsAllowedManagerNames()↔All()parity;TestIsAllowedManagerNamecovers traversal payloads (../etc/passwd,/etc/passwd,../../tmp/evil,..\..\evil,./fnm), case-fold negativity (FNM,FnM), near-miss strings (fnmm,fn,lol,fnm,fnm/), and a sanity check that every name inAllowedManagerNames()passes the boolean helper.internal/cli/packages.go—runRestore(positional branch) andrunDiffboth bail withdetector.IsAllowedManagerName(managerName)before constructing any snapshot path. Error message surfaces the offender and the full allowlist so typos remain user-fixable.internal/cli/packages_test.go— new file. End-to-end cobracmd.Execute()tests pinning the runRestore/runDiff error message (TestRunRestore_RejectsPathTraversal,TestRunDiff_RejectsPathTraversal) plusTestRunRestore_AcceptsCanonicalNameto catch a regression that flipped the boolean to reject everything.CHANGELOG.md—### Fixedentry under[Unreleased]documenting the change with the fix(packages): path traversal via unsanitized manager name in packages restore/diff #51 reference.