Skip to content

Handle windows-style bare specifiers in exports #30169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jkrems opened this issue Jul 23, 2019 · 11 comments
Open

Handle windows-style bare specifiers in exports #30169

jkrems opened this issue Jul 23, 2019 · 11 comments
Labels
module Issues and PRs related to the module subsystem.

Comments

@jkrems
Copy link
Contributor

jkrems commented Jul 23, 2019

There's currently a loophole in CJS exports:

require('x\\y.js');

The above will load y.js at least on Windows even if x uses exports and didn't whitelist the path. This happens because the specifier isn't recognized as an exports specifier.

There's three possible ways out:

  1. We detect this case and iff x uses exports treat it as "not mapped", no matter what the subpath is.
  2. We accept this as a loophole.
  3. We normalize the specifier, turning backslashes into slashes.

There are other potential loopholes like the following:

  • @scope/../any/file.js: Doesn't match the RegExp, allows access to any/file.js.
  • TBD

It's not quite clear if we can plug all holes in CJS but there's likely some way of doing it.

@ljharb
Copy link
Member

ljharb commented Jul 23, 2019

1 or 3 sounds good to me.

@zackschuster
Copy link

zackschuster commented Jul 23, 2019

i think normalizing the specifier to the platform would be in the realm of "expected behavior"?

@jkrems
Copy link
Contributor Author

jkrems commented Jul 23, 2019

Yes-ish. The problem is that we're crossing streams. It's the same configuration for both ESM and CJS. ESM operates on file URLs so "normalizing" means converting those to platform file system paths. CJS operates on file system paths so "normalizing" means first mapping relative path segments to URL segments, then mapping, and then mapping the result back to file system paths. The question is less "should we normalize" and more "what do we consider our input/output formats to be" so we can normalize correctly.

@bmeck
Copy link
Member

bmeck commented Jul 23, 2019

  1. is the general approach wrt some things like case insensitive filesystems using require(). Seems fine.

As long as it is documented, I don't have a preference on solution chosen.

If we do go with 3. however I would suggest the following types and coercions:

  1. exports key is a relative URL string with leading . or .., or absolute URL string
  2. exports value is a relative URL string string with leading . or .., or absolute URL string

Conversion happens lazily like most things. People can use auditing like linters to do ahead of time checks.

The URLs are resolved against the location of the package.json file using the regular URL parser:

// will ensure trailing / and remove fragment and query
// don't need this if you aren't checking location after resolve
pkgRoot = new URL('.', fileURLOfPackageJSON);
resolvedExportValue = new URL(value, pkgRoot);

// could do a pathname check to see if it stayed in the pkgRoot?
// idk, seems more of an auditing problem than a need to prevent this behavior

// only used by CJS
// this strips query/fragment but CJS doesn't support those to begin with
filename = fileURLToPath(resolvedExportValue);

fileURLToPath handles some minor oddities when converting between things and new URL handles \\ normalization in a standardized way as seen with new URL('.\\x\\y', 'file:////app/').

@jkrems
Copy link
Contributor Author

jkrems commented Oct 29, 2019

My current thinking is that we should officially narrow the scope of exports to what it is right now: specifiers that are a package name or a package name followed by a slash.

This will mean that code can "exploit" backslash on Windows (under require) and code can "exploit" case-differences on case-insensitive file systems. But portable code will be using exports. Since we can't close the more general escape hatch (case-insensitive file systems), it feels weird to add complexity to close a very platform specific one. I don't think I've seen backslash widely used in imports or requires.

EDIT: Fixed some grammar.

@ljharb
Copy link
Member

ljharb commented Oct 29, 2019

By “specifiers that are a package name” you mean, exports only “kicks in” when the resolved specifier started with a bare specifier?

@jkrems
Copy link
Contributor Author

jkrems commented Oct 29, 2019

Yep. Or to be more precise: when it's a "package name specifier" which is a subset of all bare specifiers. E.g. \0%$! is a bare specifier but not something that would trigger exports (a "package name").

@MylesBorins
Copy link
Contributor

MylesBorins commented Oct 29, 2019 via email

@jkrems jkrems transferred this issue from nodejs/modules Oct 29, 2019
@docmaster2
Copy link

одно спотыкание и мы<clang-format.json>

@targos targos added the module Issues and PRs related to the module subsystem. label Dec 26, 2020
@targos
Copy link
Member

targos commented Dec 26, 2020

Is this still relevant?

@targos
Copy link
Member

targos commented Feb 20, 2021

I confirm the behavior is still there. Option 3 makes sense in my opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module Issues and PRs related to the module subsystem.
Projects
None yet
Development

No branches or pull requests

7 participants