Skip to content
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

esm: Ensure custom loader resolved "url" is properly validated #21352

Closed
wants to merge 8 commits into from

Conversation

guybedford
Copy link
Contributor

This ensures that any loader returning a non-valid URL from the resolve hook will throw an error in the main resolution.

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

@@ -0,0 +1,8 @@
export async function resolve(specifier, parentModuleURL, defaultResolve) {
if (parentModuleURL && specifier !== 'assert')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: braces around multi-line condition body

new URL(url);
} catch (err) {
throw new ERR_INVALID_URL(url);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm... I'm not sure what the benefit of the try/catch and additional throw here. If the url is invalid, new URL(url) will throw and accomplish the same thing, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well caught, yes that's simpler. Updated.

@guybedford
Copy link
Contributor Author

@@ -64,6 +65,11 @@ class Loader {
if (format === 'builtin')
return { url: `node:${url}`, format };

if (this._resolve !== defaultResolve) {
// throws ERR_INVALID_URL for resolve if not valid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here's one of the places we can use a more descriptive error message. Something like "invalid URL returned by loader" is preferred over the user having to research why an error is thrown.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stack trace should be enough right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so in this case, because it's not clear what the call hierarchy is. For example, if it's coming from the URL constructor, then it's pretty obvious. But no one would know what Loader.resolve is without reading the source code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I've done some polishing on the exact error messages.

assert.fail();
}, (err) => {
assert.strictEqual(err.code, 'ERR_INVALID_URL');
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume we would want .then(common.mustCall()) here?

@guybedford guybedford force-pushed the loader-url-validate branch 3 times, most recently from 65f89b7 to ea909c3 Compare June 17, 2018 14:37
Copy link
Member

@TimothyGu TimothyGu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few nits. LGTM functionality-wise.

@@ -0,0 +1,12 @@
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs
/* eslint-disable node-core/required-modules */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move the import common to before assert to get rid of this override?

@@ -0,0 +1,9 @@
export async function resolve(specifier, parentModuleURL, defaultResolve) {
if (parentModuleURL && specifier !== 'assert') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have to exclude ../common as well?

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if overloading the ERR_INVALID_RETURN_VALUE would be best for all use cases here. And another test to check the error in case of a returned primitive would be nice as well.

import common from './index.js';

const { mustCall } = common;
export { mustCall };

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ./index.js is imported, the already existing functionality here should be removed as that would be duplicated.

I suggest to export all common functions and to remove everything else from this file. I personally would move that code change to a individual commit.

assert.fail();
}, (err) => {
assert.strictEqual(err.code, 'ERR_INVALID_RETURN_PROPERTY_STRING');
})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to use common.expectsError here instead of the second function. That way the error message itself could also be verified.

The code could look like e.g.

.then(
  assert.fail,
  common.expectsError({
    code: 'ERR_INVALID_RETURN_PROPERTY_STRING',
    message: '...'
  })
).then(mustCall());

return `Expected ${input} to be returned from the "${name}"` +
` function but got ${type}.`;
return `Expected ${input} to be returned from the ` +
`"${name}" function but got ${type}.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is a unrelated change.

}, TypeError);
E('ERR_INVALID_RETURN_PROPERTY_STRING', (input, name, prop, value) => {
return `Expected a valid ${input} to be returned for the ${prop} from the ` +
`"${name}" function but got ${value}.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If this error code should still be kept, I suggest to change it to a string by reordering the arguments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is quite a complex arguments case, I think it is important to have the labelled arguments and also the order is important to the message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already thought so but all other errors work like that and it should still be relatively clear what it is about.

@@ -56,16 +58,32 @@ class Loader {
await this._resolve(specifier, parentURL, defaultResolve);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to validate that the return type is actually an object. Otherwise the following error messages would still be weird (e.g., in case 5 is returned the error message would be about the url property).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I didn't realise destructuring didn't throw for such cases actually.

throw new ERR_INVALID_ARG_TYPE('format', 'string', format);
throw new ERR_INVALID_RETURN_PROPERTY_STRING(
'string', 'loader resolve', 'format', format
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error should likely be ERR_INVALID_RETURN_PROPERTY just as the error above. Or I fail to see why this is handled differently than the url error case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first error checks that it is a string, the second error checks that it is the right type of string.

The messages are:

  1. Expected string to be returned for the url from the "loader resolve" function but got ${type}.
  2. Expected a valid url to be returned for the url from the "loader resolve" function but got ${value}.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see the second case being related to this error. It is about format?

Copy link
Contributor Author

@guybedford guybedford Jun 18, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, I missed this was specifically the format case, but the same argument applied - format must be one of a specific set of string values, so the error message will say:

Expected a valid format to be returned for the format from the "loader resolve" function but got ${value}

instead of

Expected string to be returned for the format from the "loader resolve" function but got string.

### ERR_INVALID_RETURN_PROPERTY_STRING

Thrown in case a function option does not return an expected string property
type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure why strings got special handled while other types not. I recommend to change this to: ERR_INVALID_RETURN_PROPERTY_VALUE.
The current description is definitely wrong as the usage is not about the returned type but about the returned value (at least when looking at the actual implementation).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the semantics, but the thing is ideally the current ERR_INVALID_RETURN_VALUE error should then be ERR_INVALID_RETURN_TYPE as that is what it is really checking. So I was trying to remain consistent with that. I think ERR_INVALID_RETURN_PROPERTY_VALUE isn't suitable for this reason. type in the description is referring to "types of strings" as in categories / enums.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the ERR_INVALID_RETURN_VALUE could be improved. However the return checks are not common and could be consolidated to always be a ERR_INVALID_RETURN_VALUE, just with a more specific error message. IMHO differentiating the type error from the value error is not useful for users.

That all aside: I am definitely against having a error code that is specific for property strings. So at least that has to be more generalized out of my perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO differentiating the type error from the value error is not useful for users.

See my example response in the last comment for why I definitely feel this is a useful distinction in the error message.

In terms of naming, I've switched them around to use ERR_INVALID_RETURN_PROPERTY_VALUE to be consistent with return value, then made the new one just ERR_INVALID_RETURN_PROPERTY.

@@ -1204,6 +1204,17 @@ An invalid `options.protocol` was passed.
Both `breakEvalOnSigint` and `eval` options were set in the REPL config, which
is not supported.

<a id="ERR_INVALID_RETURN_PROPERTY"></a>
### ERR_INVALID_RETURN_PROPERTY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name here might be better as: ERR_INVALID_RETURN_PROPERTY_TYPE?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, the consistent name here would be ERR_INVALID_RETURN_PROPERTY_VALUE actually :)

}, TypeError);
E('ERR_INVALID_RETURN_PROPERTY_STRING', (input, name, prop, value) => {
return `Expected a valid ${input} to be returned for the ${prop} from the ` +
`"${name}" function but got ${value}.`;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already thought so but all other errors work like that and it should still be relatively clear what it is about.

throw new ERR_INVALID_ARG_TYPE('format', 'string', format);
throw new ERR_INVALID_RETURN_PROPERTY_STRING(
'string', 'loader resolve', 'format', format
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see the second case being related to this error. It is about format?

restoreStdout,
restoreStderr,
isCPPSymbolsNotMapped
} = common;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be sufficient to just export common, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only do this once we have named exports support for CommonJS modules.

### ERR_INVALID_RETURN_PROPERTY_STRING

Thrown in case a function option does not return an expected string property
type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the ERR_INVALID_RETURN_VALUE could be improved. However the return checks are not common and could be consolidated to always be a ERR_INVALID_RETURN_VALUE, just with a more specific error message. IMHO differentiating the type error from the value error is not useful for users.

That all aside: I am definitely against having a error code that is specific for property strings. So at least that has to be more generalized out of my perspective.

@guybedford
Copy link
Contributor Author

@guybedford
Copy link
Contributor Author

@BridgeAR let me know if you have further feedback on the error cases, otherwise could you mark your review on the error adjustments here as approved?

@guybedford
Copy link
Contributor Author

Will merge this Monday / Tuesday if there is no further feedback.

Copy link
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to get some further tests in to actually check for all the new error cases before this lands.

throw new ERR_INVALID_ARG_TYPE('format', 'string', format);
throw new ERR_INVALID_RETURN_PROPERTY(
'module format', 'loader resolve', 'format', format
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not certain why typeof format !== 'string' is an ERR_INVALID_RETURN_PROPERTY while typeof url !== 'string' is an ERR_INVALID_RETURN_PROPERTY_VALUE. The validation step seems the same so it is somewhat weird for me that it results in different errors.

@guybedford
Copy link
Contributor Author

@BridgeAR thanks, I've corrected the inconsistency there and added a test. Would be good to get this in soon if you can confirm a final review.

@guybedford
Copy link
Contributor Author

@jasnell
Copy link
Member

jasnell commented Jun 29, 2018

Failure on the one linux bot appears unrelated, but just to be safe: https://ci.nodejs.org/job/node-test-commit-linux/19847/

(am I doing it right @Trott ;-P ...)

@Trott
Copy link
Member

Trott commented Jun 29, 2018

(am I doing it right @Trott ;-P ...)

That link seems to be to a CI job I started, not one that you restarted.

In this particular case, I'd go to the node-test-pull-request job and use "Resume Build". That will create a new node-test-pull-request job with all the green stuff preserved (no re-run of it) but anything else (yellow, red, grey) re-run.

I just did that, and here's the resulting node-test-pull-request:

https://ci.nodejs.org/job/node-test-pull-request/15673/

guybedford added a commit that referenced this pull request Jun 30, 2018
PR-URL: #21352
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
@guybedford
Copy link
Contributor Author

@jasnell thanks for the bump on this, and nice to know the trick to get CI green there.

I went ahead and merged in 1bf42f4.

@guybedford guybedford closed this Jun 30, 2018
@targos targos added esm Issues and PRs related to the ECMAScript Modules implementation. test Issues and PRs related to the tests. backport-requested-v10.x labels Jun 30, 2018
@targos
Copy link
Member

targos commented Jun 30, 2018

This lands cleanly on v10.x-staging but the test fails.

Maybe it depends on a previous change that wasn't backported yet? See https://github.com/nodejs/node/issues?q=label%3Abackport-requested-v10.x+is%3Aclosed+sort%3Aupdated-desc for the list of PRs waiting for a backport.

=== release test-esm-loader-invalid-format ===
Path: es-module/test-esm-loader-invalid-format
Mismatched noop function calls. Expected exactly 1, actual 0.
    at exports.mustCall (/home/mzasso/git/nodejs/v10.x/test/common/index.js:428:10)
    at file:///home/mzasso/git/nodejs/v10.x/test/es-module/test-esm-loader-invalid-format.mjs:11:7
    at ModuleJob.run (internal/modules/esm/module_job.js:96:12)
(node:1657) ExperimentalWarning: The ESM module loader is experimental.
(node:1657) UnhandledPromiseRejectionWarning: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
+ expected - actual

  Comparison {
-   code: 'ERR_INVALID_RETURN_PROPERTY_VALUE',
-   message: 'Expected string to be returned for the "format" from the "loader resolve" function but got type undefined.'
+   code: 'ERR_INVALID_RETURN_PROPERTY',
+   message: 'Expected string to be returned for the "url" from the "loader resolve" function but got "undefined"'
  }
    at new AssertionError (internal/assert.js:268:11)
    at innerFn (/home/mzasso/git/nodejs/v10.x/test/common/index.js:752:15)
    at /home/mzasso/git/nodejs/v10.x/test/common/index.js:468:15
(node:1657) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:1657) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Command: out/Release/node --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-format.mjs /home/mzasso/git/nodejs/v10.x/test/es-module/test-esm-loader-invalid-format.mjs

@targos
Copy link
Member

targos commented Jun 30, 2018

addaleax added a commit to addaleax/node that referenced this pull request Jun 30, 2018
The previously landed commit was broken and it’s too late
to force-push. Fixing up the test seems to work.

Refs: nodejs#21352
addaleax added a commit that referenced this pull request Jun 30, 2018
The previously landed commit was broken and it’s too late
to force-push. Fixing up the test seems to work.

Refs: #21352

PR-URL: #21605
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
targos pushed a commit that referenced this pull request Jun 30, 2018
PR-URL: #21352
Reviewed-By: Gus Caplan <me@gus.host>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
targos pushed a commit that referenced this pull request Jun 30, 2018
The previously landed commit was broken and it’s too late
to force-push. Fixing up the test seems to work.

Refs: #21352

PR-URL: #21605
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
@targos targos mentioned this pull request Jul 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. test Issues and PRs related to the tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants