fix(review): track lockfile entry nesting so a nested dependencies object can't hide tamper (#5837) - #6050
Closed
shin-core wants to merge 1 commit into
Closed
Conversation
…ject can't hide tamper (JSONbored#5837) scanPackageLockPatch tracked "which package-lock entry am I inside" with a single flat currentEntryKey/currentPackageName pointer. A real `node_modules/...` entry often contains its own nested `dependencies`/`devDependencies`/ `optionalDependencies` sub-object; hitting that nested container header reset the pointer to null and never restored it, so any `resolved`/`integrity`/`version` line for the SAME outer entry appearing after the nested object was silently skipped by the current-entry guard. Real npm output orders version/resolved/ integrity before dependencies, so legitimate diffs never tripped it — but an attacker could reorder an entry's keys to place `dependencies` first and evade the tamper detector entirely for that entry. Replace the flat pointer with a stack of brace scopes: each object-header line pushes a frame (an entry, or null for a container/root/other), each closing brace pops one, so closing a nested sub-object restores the enclosing entry as the current one. Entries are still keyed by their full `node_modules/...` path (JSONbored#2692) and the legacy lockfileVersion-1 bare-name branch is unchanged; the public API and gate wiring are untouched — this is a parsing correction only. Adds a regression test where the entry's `dependencies` object precedes its resolved/integrity/version (reordered keys), and a second where two same-named entries each carry a nested dependencies object, confirming the JSONbored#2692 keying and this fix compose.
Contributor
|
🚨 Contributor flagged. Click here for more info: Superagent Dashboard |
Contributor
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Contributor
Author
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
Closes #5837.
scanPackageLockPatchinsrc/review/lockfile-tamper.tsis the heuristic line-based scanner that flags theclassic supply-chain tell: a
resolved/integrityvalue changed for apackage-lock.jsonentry without thatsame entry's own
"version"genuinely changing. It tracked "which entry am I inside" with a single flatcurrentEntryKey/currentPackageNamepointer.A real
node_modules/...entry frequently contains its own nesteddependencies/devDependencies/optionalDependenciessub-object. Once inside thepackagestree, hitting that nestedcontainer header fell into the
elsebranch and reset the pointer tonull— and it was never restored whenthe nested object closed. Any
resolved/integrity/versionline for the same outer entry appearing after thenested sub-object was then silently skipped by the current-entry guard.
Today's npm output always orders
version/resolved/integritybeforedependencieswithin an entry, solegitimate diffs never trip this. But nothing enforces that ordering: an attacker hand-crafting a tamper diff
could simply reorder the entry's keys so
dependenciesprecedesresolved/integrity, defeating the tamperdetector entirely for that entry.
Fix: replace the flat pointer with a stack of brace scopes. Each object-header line pushes a frame (an
entry, or
nullfor a container/root/other header); each closing brace pops one. Closing a nested sub-objecttherefore restores the enclosing entry as the current one, so subsequent
resolved/integrity/versionlines are still attributed to it regardless of key order. This is a parsing/state-tracking correction only:
node_modules/...path (fix(review): stop false-positiving on transitive bumps and workspace links #2692), so two same-named entries under differentnesting paths remain independent.
dependenciestree (!sawPackagesEntrybranch) is unchanged.LockfileTamperCandidate's shape, the module's public API, and the gate/finding wiring(
isConfiguredGateBlocker) are untouched.Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Closes #123) — a linked open issue is required for every contributor PR.Validation
git diff --checknpm run actionlintnpm run typechecknpm run test:coveragelocally;codecov/patchrequires ≥99% coverage of the lines AND branches you changed (aim for 100% on your diff so CI variance does not fail near the threshold). Global coverage is a non-blocking trend with a loose 90% backstop, not the gate.npm run test:workersnpm run build:mcpnpm run test:mcp-packnpm run ui:openapi:checknpm run ui:lintnpm run ui:typechecknpm run ui:buildnpm audit --audit-level=moderateIf any required check was skipped, explain why:
npm run test:cigate plusnpm audit --audit-level=moderate(0 vulnerabilities).scanPackageLockPatchis at 100% branch coverage on the changed lines (unshardednpm run test:coverage); the two new regression tests were confirmed to FAIL against the pre-fix source, so they genuinely pin the bug.Safety
UI Evidencesection below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.Notes on the Safety boxes: this is a backend-only parsing correction to a deterministic supply-chain scanner — no UI, API/OpenAPI, or auth/CORS/session surface. The negative-path testing that applies is the scanner's own: the added tests cover the tamper-detected path (reordered keys), and the existing suite already covers the no-finding legitimate-bump path (unchanged and still passing). The fix strengthens a security check rather than relaxing one.
UI Evidence
Not applicable — backend-only change to a diff parser; no visible UI, frontend, docs, or extension surface.
Notes
test/unit/lockfile-tamper.test.ts): (1) anode_modules/lodashentry whosedependenciessub-object precedes itsresolved/integrity/versionlines (key order reversed from real npm output) — asserts the unbumped-integrity tamper signal is still detected; (2) two distinct entries sharing the bare namefoo(node_modules/fooandnode_modules/bar/node_modules/foo), each with its owndependenciessub-object — the legitimate bump on the top-levelfoodoes not mask the unbumped integrity edit on the nestedfoo, confirming the fix(review): stop false-positiving on transitive bumps and workspace links #2692 full-path keying and this nesting fix compose. Both new tests were verified to fail on the pre-fix source.