nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨

Home Page:https://nodejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Multiple slashes can bypass `null` exports path

privatenumber opened this issue · comments

Version

v16.17.0

Platform

macOS 12.4

Subsystem

No response

Description

Setting null on an export path can be used to exclude private directories from being exposed (Node.js docs):

// ./node_modules/package/package.json
{
    "exports": {
        "./*": "./*",
        "./internal/*": null
    }
}

Although this works to prevent an import like this:

import 'package/internal/file.js'

It can be circumvented by using multiple slashes:

import 'package//internal/file.js'

In UNIX, multiple consecutive slashes are treated as a single slash.

What steps will reproduce the bug?

StackBlitz reproduction

https://stackblitz.com/edit/node-auughm?file=index.js

Reproduction repo link

https://github.com/privatenumber/issue-reproductions/tree/master/reproductions/nodejs/node/44316

Steps

  1. Create the following files:

node_modules/package/package.json:

{
	"exports": {
		"./*": "./*",
		"./internal/*": null
	}
}

node_modules/package/internal/file.js:

console.log('Accessed internal file!')

index.mjs:

import('package/internal/file.js').catch(() => {
	console.log('Fail: Single slash import')
})

import('package//internal/file.js').then(() => {
	console.log('Success: Double slash import')
})
  1. Run node ./index.mjs

How often does it reproduce? Is there a required condition?

Always. No environmental conditions. Just multiple slashes in import paths.

What is the expected behavior?

For import 'package//internal/file.js' to not resolve.

What do you see instead?

It resolves.

Additional information

No response

I'm able to reproduce on v19.0.0-pre.

Seeing packageResolve() and packageExportsResolve(), the packageSubpath string should begin with any key string of packageConfig.exports to properly find the bestmatch and bestMatchSubpath. It doesn't seem to use a normalized path. Maybe that's intended or a bug.

let bestMatch = '';
let bestMatchSubpath;
const keys = ObjectGetOwnPropertyNames(exports);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const patternIndex = StringPrototypeIndexOf(key, '*');
if (patternIndex !== -1 &&
StringPrototypeStartsWith(packageSubpath,
StringPrototypeSlice(key, 0, patternIndex))) {

/cc @nodejs/loaders @nodejs/modules

It doesn't seem to use a normalized path. Maybe that's intended or a bug.

I think that's intended, IIRC everything related to "exports" is a URL (or URL pattern), not a path.

That being said, I agree that we should fix this, using one or more slashes should give the same result.