mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 10:59:27 +00:00
lib,src: drop --experimental-network-imports
PR-URL: https://github.com/nodejs/node/pull/53822 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
This commit is contained in:
parent
e4263dad96
commit
5ac969fdca
@ -965,18 +965,6 @@ changes:
|
||||
Specify the `module` containing exported [module customization hooks][].
|
||||
`module` may be any string accepted as an [`import` specifier][].
|
||||
|
||||
### `--experimental-network-imports`
|
||||
|
||||
<!-- YAML
|
||||
added:
|
||||
- v17.6.0
|
||||
- v16.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Enable experimental support for the `https:` protocol in `import` specifiers.
|
||||
|
||||
### `--experimental-network-inspection`
|
||||
|
||||
<!-- YAML
|
||||
@ -2912,7 +2900,6 @@ one is included in the list below.
|
||||
* `--experimental-json-modules`
|
||||
* `--experimental-loader`
|
||||
* `--experimental-modules`
|
||||
* `--experimental-network-imports`
|
||||
* `--experimental-permission`
|
||||
* `--experimental-print-required-tla`
|
||||
* `--experimental-require-module`
|
||||
|
@ -3583,23 +3583,6 @@ removed: v10.0.0
|
||||
|
||||
Used by the `Node-API` when `Constructor.prototype` is not an object.
|
||||
|
||||
<a id="ERR_NETWORK_IMPORT_BAD_RESPONSE"></a>
|
||||
|
||||
### `ERR_NETWORK_IMPORT_BAD_RESPONSE`
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Response was received but was invalid when importing a module over the network.
|
||||
|
||||
<a id="ERR_NETWORK_IMPORT_DISALLOWED"></a>
|
||||
|
||||
### `ERR_NETWORK_IMPORT_DISALLOWED`
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
A network module attempted to load another module that it is not allowed to
|
||||
load. Likely this restriction is for security reasons.
|
||||
|
||||
<a id="ERR_NO_LONGER_SUPPORTED"></a>
|
||||
|
||||
### `ERR_NO_LONGER_SUPPORTED`
|
||||
|
@ -697,71 +697,6 @@ spawn(execPath, [
|
||||
});
|
||||
```
|
||||
|
||||
## HTTPS and HTTP imports
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Importing network based modules using `https:` and `http:` is supported under
|
||||
the `--experimental-network-imports` flag. This allows web browser-like imports
|
||||
to work in Node.js with a few differences due to application stability and
|
||||
security concerns that are different when running in a privileged environment
|
||||
instead of a browser sandbox.
|
||||
|
||||
### Imports are limited to HTTP/1
|
||||
|
||||
Automatic protocol negotiation for HTTP/2 and HTTP/3 is not yet supported.
|
||||
|
||||
### HTTP is limited to loopback addresses
|
||||
|
||||
`http:` is vulnerable to man-in-the-middle attacks and is not allowed to be
|
||||
used for addresses outside of the IPv4 address `127.0.0.0/8` (`127.0.0.1` to
|
||||
`127.255.255.255`) and the IPv6 address `::1`. Support for `http:` is intended
|
||||
to be used for local development.
|
||||
|
||||
### Authentication is never sent to the destination server.
|
||||
|
||||
`Authorization`, `Cookie`, and `Proxy-Authorization` headers are not sent to the
|
||||
server. Avoid including user info in parts of imported URLs. A security model
|
||||
for safely using these on the server is being worked on.
|
||||
|
||||
### CORS is never checked on the destination server
|
||||
|
||||
CORS is designed to allow a server to limit the consumers of an API to a
|
||||
specific set of hosts. This is not supported as it does not make sense for a
|
||||
server-based implementation.
|
||||
|
||||
### Cannot load non-network dependencies
|
||||
|
||||
These modules cannot access other modules that are not over `http:` or `https:`.
|
||||
To still access local modules while avoiding the security concern, pass in
|
||||
references to the local dependencies:
|
||||
|
||||
```mjs
|
||||
// file.mjs
|
||||
import worker_threads from 'node:worker_threads';
|
||||
import { configure, resize } from 'https://example.com/imagelib.mjs';
|
||||
configure({ worker_threads });
|
||||
```
|
||||
|
||||
```mjs
|
||||
// https://example.com/imagelib.mjs
|
||||
let worker_threads;
|
||||
export function configure(opts) {
|
||||
worker_threads = opts.worker_threads;
|
||||
}
|
||||
export function resize(img, size) {
|
||||
// Perform resizing in worker_thread to avoid main thread blocking
|
||||
}
|
||||
```
|
||||
|
||||
### Network-based loading is not enabled by default
|
||||
|
||||
For now, the `--experimental-network-imports` flag is required to enable loading
|
||||
resources over `http:` or `https:`. In the future, a different mechanism will be
|
||||
used to enforce this. Opt-in is required to prevent transitive dependencies
|
||||
inadvertently using potentially mutable state that could affect reliability
|
||||
of Node.js applications.
|
||||
|
||||
<i id="esm_experimental_loaders"></i>
|
||||
|
||||
## Loaders
|
||||
@ -804,8 +739,7 @@ does not determine whether the resolved URL protocol can be loaded,
|
||||
or whether the file extensions are permitted, instead these validations
|
||||
are applied by Node.js during the load phase
|
||||
(for example, if it was asked to load a URL that has a protocol that is
|
||||
not `file:`, `data:`, `node:`, or if `--experimental-network-imports`
|
||||
is enabled, `https:`).
|
||||
not `file:`, `data:` or `node:`.
|
||||
|
||||
The algorithm also tries to determine the format of the file based
|
||||
on the extension (see `ESM_FILE_FORMAT` algorithm below). If it does
|
||||
|
@ -717,7 +717,7 @@ behaviors.
|
||||
#### Import from HTTPS
|
||||
|
||||
In current Node.js, specifiers starting with `https://` are experimental (see
|
||||
[HTTPS and HTTP imports][]).
|
||||
\[HTTPS and HTTP imports]\[]).
|
||||
|
||||
The hook below registers hooks to enable rudimentary support for such
|
||||
specifiers. While this may seem like a significant improvement to Node.js core
|
||||
@ -1054,7 +1054,6 @@ returned object contains the following keys:
|
||||
[Conditional exports]: packages.md#conditional-exports
|
||||
[Customization hooks]: #customization-hooks
|
||||
[ES Modules]: esm.md
|
||||
[HTTPS and HTTP imports]: esm.md#https-and-http-imports
|
||||
[Source map v3 format]: https://sourcemaps.info/spec.html#h.mofvlxcwqzej
|
||||
[`"exports"`]: packages.md#exports
|
||||
[`--enable-source-maps`]: cli.md#--enable-source-maps
|
||||
|
@ -173,9 +173,6 @@ Specify the
|
||||
.Ar module
|
||||
to use as a custom module loader.
|
||||
.
|
||||
.It Fl -experimental-network-imports
|
||||
Enable experimental support for loading modules using `import` over `https:`.
|
||||
.
|
||||
.It Fl -experimental-permission
|
||||
Enable the experimental permission model.
|
||||
.
|
||||
|
@ -1591,10 +1591,6 @@ E('ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT',
|
||||
'start offset of %s should be a multiple of %s', RangeError);
|
||||
E('ERR_NAPI_INVALID_TYPEDARRAY_LENGTH',
|
||||
'Invalid typed array length', RangeError);
|
||||
E('ERR_NETWORK_IMPORT_BAD_RESPONSE',
|
||||
"import '%s' received a bad response: %s", Error);
|
||||
E('ERR_NETWORK_IMPORT_DISALLOWED',
|
||||
"import of '%s' by %s is not supported: %s", Error);
|
||||
E('ERR_NOT_BUILDING_SNAPSHOT',
|
||||
'Operation cannot be invoked when not building startup snapshot', Error);
|
||||
E('ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION',
|
||||
|
@ -2,8 +2,6 @@
|
||||
|
||||
const {
|
||||
ObjectPrototypeHasOwnProperty,
|
||||
PromisePrototypeThen,
|
||||
PromiseResolve,
|
||||
RegExpPrototypeExec,
|
||||
SafeSet,
|
||||
StringPrototypeCharCodeAt,
|
||||
@ -18,8 +16,6 @@ const {
|
||||
} = require('internal/modules/esm/formats');
|
||||
|
||||
const detectModule = getOptionValue('--experimental-detect-module');
|
||||
const experimentalNetworkImports =
|
||||
getOptionValue('--experimental-network-imports');
|
||||
const { containsModuleSyntax } = internalBinding('contextify');
|
||||
const { getPackageScopeConfig, getPackageType } = require('internal/modules/package_json_reader');
|
||||
const { fileURLToPath } = require('internal/url');
|
||||
@ -29,8 +25,6 @@ const protocolHandlers = {
|
||||
'__proto__': null,
|
||||
'data:': getDataProtocolModuleFormat,
|
||||
'file:': getFileProtocolModuleFormat,
|
||||
'http:': getHttpProtocolModuleFormat,
|
||||
'https:': getHttpProtocolModuleFormat,
|
||||
'node:'() { return 'builtin'; },
|
||||
};
|
||||
|
||||
@ -219,23 +213,6 @@ function getFileProtocolModuleFormat(url, context = { __proto__: null }, ignoreE
|
||||
throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {{parentURL: string}} context
|
||||
* @returns {Promise<string> | undefined} only works when enabled
|
||||
*/
|
||||
function getHttpProtocolModuleFormat(url, context) {
|
||||
if (experimentalNetworkImports) {
|
||||
const { fetchModule } = require('internal/modules/esm/fetch_module');
|
||||
return PromisePrototypeThen(
|
||||
PromiseResolve(fetchModule(url, context)),
|
||||
(entry) => {
|
||||
return mimeToFormat(entry.headers['content-type']);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @param {{parentURL: string}} context
|
||||
|
@ -1,7 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypePush,
|
||||
RegExpPrototypeExec,
|
||||
decodeURIComponent,
|
||||
} = primordials;
|
||||
@ -12,8 +11,6 @@ const { validateAttributes, emitImportAssertionWarning } = require('internal/mod
|
||||
const { getOptionValue } = require('internal/options');
|
||||
const { readFileSync } = require('fs');
|
||||
|
||||
const experimentalNetworkImports =
|
||||
getOptionValue('--experimental-network-imports');
|
||||
const defaultType =
|
||||
getOptionValue('--experimental-default-type');
|
||||
|
||||
@ -39,7 +36,7 @@ const DATA_URL_PATTERN = /^[^/]+\/[^,;]+(?:[^,]*?)(;base64)?,([\s\S]*)$/;
|
||||
*/
|
||||
async function getSource(url, context) {
|
||||
const { protocol, href } = url;
|
||||
let responseURL = href;
|
||||
const responseURL = href;
|
||||
let source;
|
||||
if (protocol === 'file:') {
|
||||
const { readFile: readFileAsync } = require('internal/fs/promises').exports;
|
||||
@ -51,19 +48,8 @@ async function getSource(url, context) {
|
||||
}
|
||||
const { 1: base64, 2: body } = match;
|
||||
source = BufferFrom(decodeURIComponent(body), base64 ? 'base64' : 'utf8');
|
||||
} else if (experimentalNetworkImports && (
|
||||
protocol === 'https:' ||
|
||||
protocol === 'http:'
|
||||
)) {
|
||||
const { fetchModule } = require('internal/modules/esm/fetch_module');
|
||||
const res = await fetchModule(url, context);
|
||||
source = await res.body;
|
||||
responseURL = res.resolvedHREF;
|
||||
} else {
|
||||
const supportedSchemes = ['file', 'data'];
|
||||
if (experimentalNetworkImports) {
|
||||
ArrayPrototypePush(supportedSchemes, 'http', 'https');
|
||||
}
|
||||
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(url, supportedSchemes);
|
||||
}
|
||||
return { __proto__: null, responseURL, source };
|
||||
@ -121,7 +107,7 @@ async function defaultLoad(url, context = kEmptyObject) {
|
||||
|
||||
const urlInstance = new URL(url);
|
||||
|
||||
throwIfUnsupportedURLScheme(urlInstance, experimentalNetworkImports);
|
||||
throwIfUnsupportedURLScheme(urlInstance);
|
||||
|
||||
if (urlInstance.protocol === 'node:') {
|
||||
source = null;
|
||||
@ -224,9 +210,8 @@ function defaultLoadSync(url, context = kEmptyObject) {
|
||||
* throws an error if the protocol is not one of the protocols
|
||||
* that can be loaded in the default loader
|
||||
* @param {URL} parsed
|
||||
* @param {boolean} experimentalNetworkImports
|
||||
*/
|
||||
function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
|
||||
function throwIfUnsupportedURLScheme(parsed) {
|
||||
// Avoid accessing the `protocol` property due to the lazy getters.
|
||||
const protocol = parsed?.protocol;
|
||||
if (
|
||||
@ -235,17 +220,11 @@ function throwIfUnsupportedURLScheme(parsed, experimentalNetworkImports) {
|
||||
protocol !== 'data:' &&
|
||||
protocol !== 'node:' &&
|
||||
(
|
||||
!experimentalNetworkImports ||
|
||||
(
|
||||
protocol !== 'https:' &&
|
||||
protocol !== 'http:'
|
||||
)
|
||||
protocol !== 'https:' &&
|
||||
protocol !== 'http:'
|
||||
)
|
||||
) {
|
||||
const schemes = ['file', 'data', 'node'];
|
||||
if (experimentalNetworkImports) {
|
||||
ArrayPrototypePush(schemes, 'https', 'http');
|
||||
}
|
||||
throw new ERR_UNSUPPORTED_ESM_URL_SCHEME(parsed, schemes);
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ const {
|
||||
} = require('internal/errors').codes;
|
||||
const { getOptionValue } = require('internal/options');
|
||||
const { isURL, pathToFileURL, URLParse } = require('internal/url');
|
||||
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const {
|
||||
compileSourceTextModule,
|
||||
getDefaultConditions,
|
||||
@ -145,9 +145,6 @@ class ModuleLoader {
|
||||
#customizations;
|
||||
|
||||
constructor(customizations) {
|
||||
if (getOptionValue('--experimental-network-imports')) {
|
||||
emitExperimentalWarning('Network Imports');
|
||||
}
|
||||
this.setCustomizations(customizations);
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,6 @@ const { getOptionValue } = require('internal/options');
|
||||
const { sep, posix: { relative: relativePosixPath }, resolve } = require('path');
|
||||
const preserveSymlinks = getOptionValue('--preserve-symlinks');
|
||||
const preserveSymlinksMain = getOptionValue('--preserve-symlinks-main');
|
||||
const experimentalNetworkImports =
|
||||
getOptionValue('--experimental-network-imports');
|
||||
const inputTypeFlag = getOptionValue('--input-type');
|
||||
const { URL, pathToFileURL, fileURLToPath, isURL, URLParse } = require('internal/url');
|
||||
const { getCWDURL, setOwnProperty } = require('internal/util');
|
||||
@ -48,7 +46,6 @@ const {
|
||||
ERR_PACKAGE_PATH_NOT_EXPORTED,
|
||||
ERR_UNSUPPORTED_DIR_IMPORT,
|
||||
ERR_UNSUPPORTED_RESOLVE_REQUEST,
|
||||
ERR_NETWORK_IMPORT_DISALLOWED,
|
||||
} = require('internal/errors').codes;
|
||||
|
||||
const { Module: CJSModule } = require('internal/modules/cjs/loader');
|
||||
@ -886,10 +883,6 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
|
||||
StringPrototypeSlice(base, 0, StringPrototypeIndexOf(base, ':') + 1) :
|
||||
base.protocol;
|
||||
const isData = protocol === 'data:';
|
||||
const isRemote =
|
||||
isData ||
|
||||
protocol === 'http:' ||
|
||||
protocol === 'https:';
|
||||
// Order swapped from spec for minor perf gain.
|
||||
// Ok since relative URLs cannot parse as URLs.
|
||||
let resolved;
|
||||
@ -907,7 +900,7 @@ function moduleResolve(specifier, base, conditions, preserveSymlinks) {
|
||||
try {
|
||||
resolved = new URL(specifier);
|
||||
} catch (cause) {
|
||||
if (isRemote && !BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
|
||||
if (isData && !BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
|
||||
const error = new ERR_UNSUPPORTED_RESOLVE_REQUEST(specifier, base);
|
||||
setOwnProperty(error, 'cause', cause);
|
||||
throw error;
|
||||
@ -976,57 +969,6 @@ function resolveAsCommonJS(specifier, parentURL) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Throw an error if an import is not allowed.
|
||||
* TODO(@JakobJingleheimer): de-dupe `specifier` & `parsed`
|
||||
* @param {string} specifier - The import specifier.
|
||||
* @param {URL} parsed - The parsed URL of the import specifier.
|
||||
* @param {URL} parsedParentURL - The parsed URL of the parent module.
|
||||
* @throws {ERR_NETWORK_IMPORT_DISALLOWED} - If the import is disallowed.
|
||||
*/
|
||||
function checkIfDisallowedImport(specifier, parsed, parsedParentURL) {
|
||||
if (parsedParentURL) {
|
||||
// Avoid accessing the `protocol` property due to the lazy getters.
|
||||
const parentProtocol = parsedParentURL.protocol;
|
||||
if (
|
||||
parentProtocol === 'http:' ||
|
||||
parentProtocol === 'https:'
|
||||
) {
|
||||
if (shouldBeTreatedAsRelativeOrAbsolutePath(specifier)) {
|
||||
// Avoid accessing the `protocol` property due to the lazy getters.
|
||||
const parsedProtocol = parsed?.protocol;
|
||||
// data: and blob: disallowed due to allowing file: access via
|
||||
// indirection
|
||||
if (parsedProtocol &&
|
||||
parsedProtocol !== 'https:' &&
|
||||
parsedProtocol !== 'http:'
|
||||
) {
|
||||
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
||||
specifier,
|
||||
parsedParentURL,
|
||||
'remote imports cannot import from a local location.',
|
||||
);
|
||||
}
|
||||
|
||||
return { url: parsed.href };
|
||||
}
|
||||
if (BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
|
||||
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
||||
specifier,
|
||||
parsedParentURL,
|
||||
'remote imports cannot import from a local location.',
|
||||
);
|
||||
}
|
||||
|
||||
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
||||
specifier,
|
||||
parsedParentURL,
|
||||
'only relative and absolute specifiers are supported.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate user-input in `context` supplied by a custom loader.
|
||||
* @param {string | URL | undefined} parentURL - The parent URL.
|
||||
@ -1068,36 +1010,10 @@ function defaultResolve(specifier, context = {}) {
|
||||
// Avoid accessing the `protocol` property due to the lazy getters.
|
||||
protocol = parsed.protocol;
|
||||
|
||||
if (protocol === 'data:' &&
|
||||
parsedParentURL.protocol !== 'file:' &&
|
||||
experimentalNetworkImports) {
|
||||
throw new ERR_NETWORK_IMPORT_DISALLOWED(
|
||||
specifier,
|
||||
parsedParentURL,
|
||||
'import data: from a non file: is not allowed',
|
||||
);
|
||||
}
|
||||
if (protocol === 'data:' ||
|
||||
(experimentalNetworkImports &&
|
||||
(
|
||||
protocol === 'https:' ||
|
||||
protocol === 'http:'
|
||||
)
|
||||
)
|
||||
) {
|
||||
if (protocol === 'data:') {
|
||||
return { __proto__: null, url: parsed.href };
|
||||
}
|
||||
}
|
||||
// There are multiple deep branches that can either throw or return; instead
|
||||
// of duplicating that deeply nested logic for the possible returns, DRY and
|
||||
// check for a return. This seems the least gnarly.
|
||||
const maybeReturn = checkIfDisallowedImport(
|
||||
specifier,
|
||||
parsed,
|
||||
parsedParentURL,
|
||||
);
|
||||
|
||||
if (maybeReturn) { return maybeReturn; }
|
||||
|
||||
// This must come after checkIfDisallowedImport
|
||||
protocol ??= parsed?.protocol;
|
||||
|
@ -436,10 +436,6 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
kAllowedInEnvvar);
|
||||
AddAlias("--loader", "--experimental-loader");
|
||||
AddOption("--experimental-modules", "", NoOp{}, kAllowedInEnvvar);
|
||||
AddOption("--experimental-network-imports",
|
||||
"experimental https: support for the ES Module loader",
|
||||
&EnvironmentOptions::experimental_https_modules,
|
||||
kAllowedInEnvvar);
|
||||
AddOption("--experimental-wasm-modules",
|
||||
"experimental ES Module support for webassembly modules",
|
||||
&EnvironmentOptions::experimental_wasm_modules,
|
||||
|
@ -123,7 +123,6 @@ class EnvironmentOptions : public Options {
|
||||
std::string localstorage_file;
|
||||
bool experimental_global_navigator = true;
|
||||
bool experimental_global_web_crypto = true;
|
||||
bool experimental_https_modules = false;
|
||||
bool experimental_wasm_modules = false;
|
||||
bool experimental_import_meta_resolve = false;
|
||||
std::string input_type; // Value of --input-type
|
||||
|
@ -30,7 +30,6 @@ describe('ESM: warn for obsolete hooks provided', { concurrency: !process.env.TE
|
||||
'--experimental-loader',
|
||||
fileURL('es-module-loaders', 'hooks-custom.mjs'),
|
||||
],
|
||||
[/Network Imports/, '--experimental-network-imports'],
|
||||
]
|
||||
) {
|
||||
it(`should print for ${experiment.toString().replaceAll('/', '')}`, async () => {
|
||||
|
@ -1,48 +0,0 @@
|
||||
import { mustCall, spawnPromisified } from '../common/index.mjs';
|
||||
import { ok, match, notStrictEqual } from 'node:assert';
|
||||
import { spawn as spawnAsync } from 'node:child_process';
|
||||
import { execPath } from 'node:process';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
|
||||
describe('ESM: http import via CLI', { concurrency: !process.env.TEST_PARALLEL }, () => {
|
||||
const disallowedSpecifier = 'http://example.com';
|
||||
|
||||
it('should throw disallowed error for insecure protocol', async () => {
|
||||
const { code, stderr } = await spawnPromisified(execPath, [
|
||||
'--experimental-network-imports',
|
||||
'--input-type=module',
|
||||
'--eval',
|
||||
`import ${JSON.stringify(disallowedSpecifier)}`,
|
||||
]);
|
||||
|
||||
notStrictEqual(code, 0);
|
||||
|
||||
// [ERR_NETWORK_IMPORT_DISALLOWED]: import of 'http://example.com/' by
|
||||
// …/[eval1] is not supported: http can only be used to load local
|
||||
// resources (use https instead).
|
||||
match(stderr, /ERR_NETWORK_IMPORT_DISALLOWED/);
|
||||
ok(stderr.includes(disallowedSpecifier));
|
||||
});
|
||||
|
||||
it('should throw disallowed error for insecure protocol in REPL', () => {
|
||||
const child = spawnAsync(execPath, [
|
||||
'--experimental-network-imports',
|
||||
'--input-type=module',
|
||||
]);
|
||||
child.stdin.end(`import ${JSON.stringify(disallowedSpecifier)}`);
|
||||
|
||||
let stderr = '';
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stderr.on('data', (data) => stderr += data);
|
||||
child.on('close', mustCall((code, _signal) => {
|
||||
notStrictEqual(code, 0);
|
||||
|
||||
// [ERR_NETWORK_IMPORT_DISALLOWED]: import of 'http://example.com/' by
|
||||
// …/[stdin] is not supported: http can only be used to load local
|
||||
// resources (use https instead).
|
||||
match(stderr, /\[ERR_NETWORK_IMPORT_DISALLOWED\]/);
|
||||
ok(stderr.includes(disallowedSpecifier));
|
||||
}));
|
||||
});
|
||||
});
|
@ -1,311 +0,0 @@
|
||||
// Flags: --experimental-network-imports --dns-result-order=ipv4first
|
||||
import * as common from '../common/index.mjs';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import tmpdir from '../common/tmpdir.js';
|
||||
import assert from 'assert';
|
||||
import http from 'http';
|
||||
import os from 'os';
|
||||
import util from 'util';
|
||||
import { describe, it } from 'node:test';
|
||||
|
||||
if (!common.hasCrypto) {
|
||||
common.skip('missing crypto');
|
||||
}
|
||||
tmpdir.refresh();
|
||||
|
||||
const https = (await import('https')).default;
|
||||
|
||||
const createHTTPServer = http.createServer;
|
||||
|
||||
// Needed to deal w/ test certs
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
const options = {
|
||||
key: fixtures.readKey('agent1-key.pem'),
|
||||
cert: fixtures.readKey('agent1-cert.pem')
|
||||
};
|
||||
|
||||
const createHTTPSServer = https.createServer.bind(null, options);
|
||||
|
||||
|
||||
const testListeningOptions = [
|
||||
{
|
||||
hostname: 'localhost',
|
||||
listenOptions: {
|
||||
host: '127.0.0.1'
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const internalInterfaces = Object.values(os.networkInterfaces()).flat().filter(
|
||||
(iface) => iface?.internal && iface.address && !iface.scopeid
|
||||
);
|
||||
for (const iface of internalInterfaces) {
|
||||
testListeningOptions.push({
|
||||
hostname: iface?.family === 'IPv6' ? `[${iface?.address}]` : iface?.address,
|
||||
listenOptions: {
|
||||
host: iface?.address,
|
||||
ipv6Only: iface?.family === 'IPv6'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const { protocol, createServer } of [
|
||||
{ protocol: 'http:', createServer: createHTTPServer },
|
||||
{ protocol: 'https:', createServer: createHTTPSServer },
|
||||
]) {
|
||||
const body = `
|
||||
export default (a) => () => a;
|
||||
export let url = import.meta.url;
|
||||
`;
|
||||
|
||||
const base = 'http://127.0.0.1';
|
||||
for (const { hostname, listenOptions } of testListeningOptions) {
|
||||
const host = new URL(base);
|
||||
host.protocol = protocol;
|
||||
host.hostname = hostname;
|
||||
// /not-found is a 404
|
||||
// ?redirect causes a redirect, no body. JSON.parse({status:number,location:string})
|
||||
// ?mime sets the content-type, string
|
||||
// ?body sets the body, string
|
||||
const server = createServer(function(_req, res) {
|
||||
const url = new URL(_req.url, host);
|
||||
const redirect = url.searchParams.get('redirect');
|
||||
|
||||
if (url.pathname === 'json') {
|
||||
common.mustCall(() => assert.strictEqual(_req.header.content, 'application/json,*/*;charset=utf-8;q=0.5'));
|
||||
}
|
||||
|
||||
if (url.pathname === '/not-found') {
|
||||
res.writeHead(404);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
if (redirect) {
|
||||
const { status, location } = JSON.parse(redirect);
|
||||
res.writeHead(status, {
|
||||
location
|
||||
});
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'content-type': url.searchParams.get('mime') || 'text/javascript'
|
||||
});
|
||||
res.end(url.searchParams.get('body') || body);
|
||||
});
|
||||
|
||||
const listen = util.promisify(server.listen.bind(server));
|
||||
await listen({
|
||||
...listenOptions,
|
||||
port: 0
|
||||
});
|
||||
const url = new URL(host);
|
||||
url.port = server?.address()?.port;
|
||||
|
||||
const ns = await import(url.href);
|
||||
assert.strict.deepStrictEqual(Object.keys(ns), ['default', 'url']);
|
||||
const obj = {};
|
||||
assert.strict.equal(ns.default(obj)(), obj);
|
||||
assert.strict.equal(ns.url, url.href);
|
||||
|
||||
// Redirects have same import.meta.url but different cache
|
||||
// entry on Web
|
||||
const redirect = new URL(url.href);
|
||||
redirect.searchParams.set('redirect', JSON.stringify({
|
||||
status: 302,
|
||||
location: url.href
|
||||
}));
|
||||
const redirectedNS = await import(redirect.href);
|
||||
assert.strict.deepStrictEqual(
|
||||
Object.keys(redirectedNS),
|
||||
['default', 'url']
|
||||
);
|
||||
assert.strict.notEqual(redirectedNS.default, ns.default);
|
||||
assert.strict.equal(redirectedNS.url, url.href);
|
||||
|
||||
// Redirects have the same import.meta.url but different cache
|
||||
// entry on Web
|
||||
const relativeAfterRedirect = new URL(url.href + 'foo/index.js');
|
||||
const redirected = new URL(url.href + 'bar/index.js');
|
||||
redirected.searchParams.set('body', 'export let relativeDepURL = (await import("./baz.js")).url');
|
||||
relativeAfterRedirect.searchParams.set('redirect', JSON.stringify({
|
||||
status: 302,
|
||||
location: redirected.href
|
||||
}));
|
||||
const relativeAfterRedirectedNS = await import(relativeAfterRedirect.href);
|
||||
assert.strict.equal(
|
||||
relativeAfterRedirectedNS.relativeDepURL,
|
||||
url.href + 'bar/baz.js'
|
||||
);
|
||||
|
||||
const unsupportedMIME = new URL(url.href);
|
||||
unsupportedMIME.searchParams.set('mime', 'application/node');
|
||||
unsupportedMIME.searchParams.set('body', '');
|
||||
await assert.rejects(
|
||||
import(unsupportedMIME.href),
|
||||
{ code: 'ERR_UNKNOWN_MODULE_FORMAT' }
|
||||
);
|
||||
|
||||
const notFound = new URL(url.href);
|
||||
notFound.pathname = '/not-found';
|
||||
await assert.rejects(
|
||||
import(notFound.href),
|
||||
{ code: 'ERR_MODULE_NOT_FOUND' },
|
||||
);
|
||||
|
||||
const jsonUrl = new URL(url.href + 'json');
|
||||
jsonUrl.searchParams.set('mime', 'application/json');
|
||||
jsonUrl.searchParams.set('body', '{"x": 1}');
|
||||
const json = await import(jsonUrl.href, { with: { type: 'json' } });
|
||||
assert.deepStrictEqual(Object.keys(json), ['default']);
|
||||
assert.strictEqual(json.default.x, 1);
|
||||
|
||||
await describe('guarantee data url will not bypass import restriction', () => {
|
||||
it('should not be bypassed by cross protocol redirect', async () => {
|
||||
const crossProtocolRedirect = new URL(url.href);
|
||||
crossProtocolRedirect.searchParams.set('redirect', JSON.stringify({
|
||||
status: 302,
|
||||
location: 'data:text/javascript,'
|
||||
}));
|
||||
await assert.rejects(
|
||||
import(crossProtocolRedirect.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be bypassed by data URL', async () => {
|
||||
const deps = new URL(url.href);
|
||||
deps.searchParams.set('body', `
|
||||
export {data} from 'data:text/javascript,export let data = 1';
|
||||
import * as http from ${JSON.stringify(url.href)};
|
||||
export {http};
|
||||
`);
|
||||
await assert.rejects(
|
||||
import(deps.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be bypassed by encodedURI import', async () => {
|
||||
const deepDataImport = new URL(url.href);
|
||||
deepDataImport.searchParams.set('body', `
|
||||
import 'data:text/javascript,import${encodeURIComponent(JSON.stringify('data:text/javascript,import "os"'))}';
|
||||
`);
|
||||
await assert.rejects(
|
||||
import(deepDataImport.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be bypassed by relative deps import', async () => {
|
||||
const relativeDeps = new URL(url.href);
|
||||
relativeDeps.searchParams.set('body', `
|
||||
import * as http from "./";
|
||||
export {http};
|
||||
`);
|
||||
const relativeDepsNS = await import(relativeDeps.href);
|
||||
assert.strict.deepStrictEqual(Object.keys(relativeDepsNS), ['http']);
|
||||
assert.strict.equal(relativeDepsNS.http, ns);
|
||||
});
|
||||
|
||||
it('should not be bypassed by file dependency import', async () => {
|
||||
const fileDep = new URL(url.href);
|
||||
const { href } = fixtures.fileURL('/es-modules/message.mjs');
|
||||
fileDep.searchParams.set('body', `
|
||||
import ${JSON.stringify(href)};
|
||||
export default 1;`);
|
||||
await assert.rejects(
|
||||
import(fileDep.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be bypassed by builtin dependency import', async () => {
|
||||
const builtinDep = new URL(url.href);
|
||||
builtinDep.searchParams.set('body', `
|
||||
import 'node:fs';
|
||||
export default 1;
|
||||
`);
|
||||
await assert.rejects(
|
||||
import(builtinDep.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should not be bypassed by unprefixed builtin dependency import', async () => {
|
||||
const unprefixedBuiltinDep = new URL(url.href);
|
||||
unprefixedBuiltinDep.searchParams.set('body', `
|
||||
import 'fs';
|
||||
export default 1;
|
||||
`);
|
||||
await assert.rejects(
|
||||
import(unprefixedBuiltinDep.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not be bypassed by indirect network import', async () => {
|
||||
const indirect = new URL(url.href);
|
||||
indirect.searchParams.set('body', `
|
||||
import childProcess from 'data:text/javascript,export { default } from "node:child_process"'
|
||||
export {childProcess};
|
||||
`);
|
||||
await assert.rejects(
|
||||
import(indirect.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('data: URL can always import other data:', async () => {
|
||||
const data = new URL('data:text/javascript,');
|
||||
data.searchParams.set('body',
|
||||
'import \'data:text/javascript,import \'data:\''
|
||||
);
|
||||
// doesn't throw
|
||||
const empty = await import(data.href);
|
||||
assert.ok(empty);
|
||||
});
|
||||
|
||||
it('data: URL cannot import file: or builtin', async () => {
|
||||
const data1 = new URL(url.href);
|
||||
data1.searchParams.set('body',
|
||||
'import \'file:///some/file.js\''
|
||||
);
|
||||
await assert.rejects(
|
||||
import(data1.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
|
||||
const data2 = new URL(url.href);
|
||||
data2.searchParams.set('body',
|
||||
'import \'node:fs\''
|
||||
);
|
||||
await assert.rejects(
|
||||
import(data2.href),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
|
||||
it('data: URL cannot import HTTP URLs', async () => {
|
||||
const module = fixtures.fileURL('/es-modules/import-data-url.mjs');
|
||||
try {
|
||||
await import(module);
|
||||
} catch (err) {
|
||||
// We only want the module to load, we don't care if the module throws an
|
||||
// error as long as the loader does not.
|
||||
assert.notStrictEqual(err?.code, 'ERR_MODULE_NOT_FOUND');
|
||||
}
|
||||
const data1 = new URL(url.href);
|
||||
const dataURL = 'data:text/javascript;export * from "node:os"';
|
||||
data1.searchParams.set('body', `export * from ${JSON.stringify(dataURL)};`);
|
||||
await assert.rejects(
|
||||
import(data1),
|
||||
{ code: 'ERR_NETWORK_IMPORT_DISALLOWED' }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
server.close();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// Flags: --experimental-require-module
|
||||
'use strict';
|
||||
|
||||
require('../common');
|
||||
const assert = require('assert');
|
||||
|
||||
assert.throws(() => {
|
||||
require('../fixtures/es-modules/network-import.mjs');
|
||||
}, {
|
||||
code: 'ERR_NETWORK_IMPORT_DISALLOWED'
|
||||
});
|
1
test/fixtures/es-modules/network-import.mjs
vendored
1
test/fixtures/es-modules/network-import.mjs
vendored
@ -1 +0,0 @@
|
||||
import 'http://example.com/foo.js';
|
Loading…
Reference in New Issue
Block a user