fix(arborist): sanitize packageName in path construction for linked strategy#9078
fix(arborist): sanitize packageName in path construction for linked strategy#9078KevinZhao wants to merge 3 commits intonpm:latestfrom
Conversation
You're gonna need to undo this in order for us to review this. The cli has its own linting rules and linter. |
c2bdfde to
9469012
Compare
|
Done — force-pushed a clean commit. The diff now contains only the |
|
This problem is solved elsewhere by using @npmcli/name-from-folder. Our pretend nodes need this, and probably need a better |
|
Thanks @wraithgar — rewritten to use The pretend node The diff is now just 3 lines changed (1 import + 2 logic changes) using the existing |
|
I think this is a problem with The isolated node class probably needs a A test may help us here figure out exactly where this is being introduced, and will also ensure the fix is doing what we expect. |
|
Thanks @wraithgar — great call on the getter approach. Changes in the latest commit:
Let me know if you'd prefer to restructure further — e.g. converting the proxy objects to IsolatedNode instances so the getter handles everything. |
|
Can we please update this branch from latest? I want to test the changes related to package name fallback removal. |
|
OK, I merged the latest branch locally and these changes test fine. |
|
Thanks @manzoorwanijk. I guess I never really took notice of how we build the store link in shrinkwrap mode. Does that look right to you that it gets put in a folder w/ a different naming convention? Is there a compelling reason we wouldn't want it to also have a key id instead? |
You're right that it uses a different convention - In practice, it works because the extraction is transient - the final store layout for the shrinkwrap's children still uses proper keys. But this is pre-existing behavior, not something introduced by this PR. |
|
Yep just wanted to bring it up and make sure it wasn't something to address. Looks like it's not something we HAVE to address immediately, if ever. Thanks for the review. |
| 'node_modules', | ||
| '.store', | ||
| `${node.packageName}@${node.version}` | ||
| `${(nameFromFolder(node.path) || node.packageName)}@${node.version}` |
There was a problem hiding this comment.
This line isn't being covered in tests because it's extremely rare for nameFromFolder to return false, basically node.path would have to be falsey.
|
Also the PR title needs a prefix:
|
448b203 to
70b3763
Compare
|
Thanks @wraithgar and @manzoorwanijk for the thorough review and for testing locally — really appreciate the detailed feedback. Rebased on latest and squashed into a single commit. Here's what changed: Code (3 lines changed in
Tests (new
To address the inline questions:
Also updated the PR title per @manzoorwanijk's suggestion. |
|
Pushed a small follow-up commit to fix the CI branch coverage failure: Root cause: The Fix: Added a test where The test diff is +11/-1 lines in |
|
Pushed a second follow-up: hardened the proxy object path in Before: result.packageName = node.packageName || nameFromFolder(node.path)After: result.packageName = nameFromFolder(node.packageName) || nameFromFolder(node.path)Why: The proxy objects (not Verified |
144b060 to
263f695
Compare
|
Squashed into a single commit for clean review. The diff is now: 3 files, +103/-1 lines:
|
|
Hi @wraithgar @manzoorwanijk — thanks again for the thorough review and testing. Just wanted to check if there's anything else needed from my side, or if this is ready for a final look. Happy to make any further changes if needed. |
manzoorwanijk
left a comment
There was a problem hiding this comment.
This looks good to me. I also tested it in Gutenberg like other recent PRs and it works fine.
|
Good point. Deleted |
|
I think you may have forgotten to push those last changes. |
263f695 to
7fe69db
Compare
| /* istanbul ignore next - packageName is always set for real packages */ | ||
| result.name = result.isWorkspace ? (node.packageName || node.name) : node.name | ||
| result.packageName = node.packageName || node.name | ||
| result.packageName = nameFromFolder(node.packageName) || nameFromFolder(node.path) |
There was a problem hiding this comment.
When is node.packageName not already cleaned at this point?
|
@wraithgar Good question. This means a I also found that the Changes pushed:
|
…trategy Node.packageName returns raw package.json name without validation (syncNormalize doesn't include fixName). This allows path traversal via malicious package names from file: deps or private registries. Changes: - Add packageName getter to IsolatedNode deriving safe names via nameFromFolder(path) - Wrap node.packageName through nameFromFolder in #assignCommonProperties so proxy objects get path-safe names - Fix hasShrinkwrap branch to use sanitized result.packageName instead of raw node.packageName in mkdirSync path construction
7be5cd7 to
a35d528
Compare
| 'node_modules', | ||
| '.store', | ||
| `${node.packageName}@${node.version}` | ||
| `${result.packageName}@${node.version}` |
There was a problem hiding this comment.
Ok I think this was the last bit of update we were missing. The common properties assignment function handles the cleaning and now we can trust result.packageName.
👍
|
This differs from the real node in a way that is likely to cause major problems if we are ever able to consolidate logic between them. I feel like we're not completely understanding the issue here and are trying to always pull the name from the path instead of using a good name when it exists, and then using name from folder when we know we have reverted to the path. |
Match the existing pattern for emulation getters (getBundler, hasInstallScript) that are never directly tested.
Per reviewer feedback, IsolatedNode.packageName should match the real Node class (return this.package.name) rather than always deriving from path. The sanitization via nameFromFolder already happens in #assignCommonProperties, so the getter doesn't need to duplicate it.
|
Good point @wraithgar — you're right that the getter should match the real Node's behavior to keep consolidation viable. Updated get packageName () {
return this.package.name || null
}The sanitization now happens exclusively in |
Summary
Add
sanitizeName()to strip path traversal sequences (../) from package names before using them in filesystem path construction inisolated-reifier.js.Package names from
package-lock.jsonare used directly inpath.join()calls, which resolves../sequences. This adds sanitization at all 9 locations wherepackageNameis interpolated into paths.Changes
sanitizeName()helper that replaces../with_and strips leading/getKey(),#generateChild(),#externalProxy(),#assignCommonProperties(),createIsolatedTree(),#processEdges(),#processDeps()Test plan
sanitizeName("../../../../tmp/foo")returns____tmp/foosanitizeName("@scope/name")returns@scope/name(scoped packages unaffected)sanitizeName("normal-package")returnsnormal-package(no-op for valid names)