diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index d7dd61819921ef..2aa5a3df4a93bd 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -1067,6 +1067,16 @@ function defaultResolve(specifier, context = {}) { if (parsed != null) { // 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 && ( @@ -1078,7 +1088,6 @@ function defaultResolve(specifier, context = {}) { 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. diff --git a/test/es-module/test-http-imports.mjs b/test/es-module/test-http-imports.mjs index e1cb1f5ced59c7..90d60e359ffd16 100644 --- a/test/es-module/test-http-imports.mjs +++ b/test/es-module/test-http-imports.mjs @@ -1,15 +1,17 @@ // Flags: --experimental-network-imports --dns-result-order=ipv4first import * as common from '../common/index.mjs'; -import { path, readKey } from '../common/fixtures.mjs'; -import { pathToFileURL } from 'url'; +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; @@ -18,8 +20,8 @@ const createHTTPServer = http.createServer; // Needed to deal w/ test certs process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; const options = { - key: readKey('agent1-key.pem'), - cert: readKey('agent1-cert.pem') + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem') }; const createHTTPSServer = https.createServer.bind(null, options); @@ -136,65 +138,6 @@ for (const { protocol, createServer } of [ url.href + 'bar/baz.js' ); - 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' } - ); - - 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}; - `); - const depsNS = await import(deps.href); - assert.strict.deepStrictEqual(Object.keys(depsNS), ['data', 'http']); - assert.strict.equal(depsNS.data, 1); - assert.strict.equal(depsNS.http, ns); - - 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); - const fileDep = new URL(url.href); - const { href } = pathToFileURL(path('/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' } - ); - - 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' } - ); - - 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' } - ); - const unsupportedMIME = new URL(url.href); unsupportedMIME.searchParams.set('mime', 'application/node'); unsupportedMIME.searchParams.set('body', ''); @@ -202,6 +145,7 @@ for (const { protocol, createServer } of [ import(unsupportedMIME.href), { code: 'ERR_UNKNOWN_MODULE_FORMAT' } ); + const notFound = new URL(url.href); notFound.pathname = '/not-found'; await assert.rejects( @@ -216,6 +160,152 @@ for (const { protocol, createServer } of [ 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(); } } diff --git a/test/fixtures/es-modules/import-data-url.mjs b/test/fixtures/es-modules/import-data-url.mjs new file mode 100644 index 00000000000000..18a2ca4486df75 --- /dev/null +++ b/test/fixtures/es-modules/import-data-url.mjs @@ -0,0 +1 @@ +import "data:text/javascript;export * from \"node:os\"";