mirror of
https://github.com/nodejs/node.git
synced 2025-12-28 07:50:41 +00:00
lib,esm: handle bypass network-import via data:
PR-URL: https://github.com/nodejs-private/node-private/pull/522 CVE-ID: CVE-2024-22020
This commit is contained in:
parent
1ba624cd3b
commit
60e184a6e4
@ -1105,9 +1105,18 @@ function defaultResolve(specifier, context = {}) {
|
||||
} else {
|
||||
parsed = new URL(specifier);
|
||||
}
|
||||
|
||||
// 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 &&
|
||||
(
|
||||
@ -1118,7 +1127,10 @@ function defaultResolve(specifier, context = {}) {
|
||||
) {
|
||||
return { __proto__: null, url: parsed.href };
|
||||
}
|
||||
} catch {
|
||||
} catch (e) {
|
||||
if (e?.code === 'ERR_NETWORK_IMPORT_DISALLOWED') {
|
||||
throw e;
|
||||
}
|
||||
// Ignore exception
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
1
test/fixtures/es-modules/import-data-url.mjs
vendored
Normal file
1
test/fixtures/es-modules/import-data-url.mjs
vendored
Normal file
@ -0,0 +1 @@
|
||||
import "data:text/javascript;export * from \"node:os\"";
|
||||
Loading…
Reference in New Issue
Block a user