UNPKG

lavamoat-core

Version:
370 lines (329 loc) 16 kB
(function () { 'use strict' return createKernelCore function createKernelCore ({ // the platform api global globalRef, // package policy object lavamoatConfig, // kernel configuration loadModuleData, getRelativeModuleId, prepareModuleInitializerArgs, getExternalCompartment, globalThisRefs, // security options scuttleGlobalThis, debugMode, runWithPrecompiledModules, reportStatsHook, }) { // prepare the LavaMoat kernel-core factory // factory is defined within a Compartment // unless "runWithPrecompiledModules" is enabled let makeKernelCore if (runWithPrecompiledModules) { makeKernelCore = unsafeMakeKernelCore } else { // endowments: // - console is included for convenience // - Math is for untamed Math.random // - Date is for untamed Date.now const kernelCompartment = new Compartment({ console, Math, Date }) makeKernelCore = kernelCompartment.evaluate(`(${unsafeMakeKernelCore})\n//# sourceURL=LavaMoat/core/kernel`) } const lavamoatKernel = makeKernelCore({ globalRef, lavamoatConfig, loadModuleData, getRelativeModuleId, prepareModuleInitializerArgs, getExternalCompartment, globalThisRefs, scuttleGlobalThis, debugMode, runWithPrecompiledModules, reportStatsHook, }) return lavamoatKernel } // this is serialized and run in a SES Compartment when "runWithPrecompiledModules" is false // mostly just exists to expose variables to internalRequire and loadBundle function unsafeMakeKernelCore ({ globalRef, lavamoatConfig, loadModuleData, getRelativeModuleId, prepareModuleInitializerArgs, getExternalCompartment, globalThisRefs = ['globalThis'], scuttleGlobalThis = {}, debugMode = false, runWithPrecompiledModules = false, reportStatsHook = () => {}, }) { // "templateRequire" calls are inlined in "generateKernel" const { getEndowmentsForConfig, getBuiltinForConfig, applyEndowmentPropDescTransforms, copyWrappedGlobals, createFunctionWrapper } = templateRequire('endowmentsToolkit')() const { prepareCompartmentGlobalFromConfig } = templateRequire('makePrepareRealmGlobalFromConfig')({ createFunctionWrapper }) const { strictScopeTerminator } = templateRequire('strict-scope-terminator') const { scuttle } = templateRequire('scuttle') const moduleCache = new Map() const packageCompartmentCache = new Map() const globalStore = new Map() const rootPackageName = '$root$' const rootPackageCompartment = createRootPackageCompartment(globalRef) scuttle(globalRef, scuttleGlobalThis) const kernel = { internalRequire, } if (debugMode) { kernel._getPolicyForPackage = getPolicyForPackage kernel._getCompartmentForPackage = getCompartmentForPackage } Object.freeze(kernel) return kernel // this function instantiaties a module from a moduleId. // 1. loads the module metadata and policy // 2. prepares the execution environment // 3. instantiates the module, recursively instantiating dependencies // 4. returns the module exports function internalRequire (moduleId) { // use cached module.exports if module is already instantiated if (moduleCache.has(moduleId)) { const moduleExports = moduleCache.get(moduleId).exports return moduleExports } reportStatsHook('start', moduleId) try { // load and validate module metadata // if module metadata is missing, throw an error const moduleData = loadModuleData(moduleId) if (!moduleData) { const err = new Error('Cannot find module \'' + moduleId + '\'') err.code = 'MODULE_NOT_FOUND' throw err } if (moduleData.id === undefined) { throw new Error('LavaMoat - moduleId is not defined correctly.') } // parse and validate module data const { package: packageName, source: moduleSource } = moduleData if (!packageName) { throw new Error(`LavaMoat - missing packageName for module "${moduleId}"`) } const packagePolicy = getPolicyForPackage(lavamoatConfig, packageName) // create the moduleObj and initializer const { moduleInitializer, moduleObj } = prepareModuleInitializer(moduleData, packagePolicy) // cache moduleObj here // this is important to inf loops when hitting cycles in the dep graph // must cache before running the moduleInitializer moduleCache.set(moduleId, moduleObj) // validate moduleInitializer if (typeof moduleInitializer !== 'function') { throw new Error(`LavaMoat - moduleInitializer is not defined correctly. got "${typeof moduleInitializer}"\n${moduleSource}`) } // initialize the module with the correct context const initializerArgs = prepareModuleInitializerArgs(requireRelativeWithContext, moduleObj, moduleData) moduleInitializer.apply(moduleObj.exports, initializerArgs) const moduleExports = moduleObj.exports return moduleExports // this is passed to the module initializer // it adds the context of the parent module // this could be replaced via "Function.prototype.bind" if its more performant // eslint-disable-next-line no-inner-declarations function requireRelativeWithContext (requestedName) { const parentModuleExports = moduleObj.exports const parentModuleData = moduleData const parentPackagePolicy = packagePolicy const parentModuleId = moduleId return requireRelative({ requestedName, parentModuleExports, parentModuleData, parentPackagePolicy, parentModuleId }) } } finally { reportStatsHook('end', moduleId) } } // this resolves a module given a requestedName (eg relative path to parent) and a parentModule context // the exports are processed via "protectExportsRequireTime" per the module's configuration function requireRelative ({ requestedName, parentModuleExports, parentModuleData, parentPackagePolicy, parentModuleId }) { const parentModulePackageName = parentModuleData.package const parentPackagesWhitelist = parentPackagePolicy.packages const parentBuiltinsWhitelist = Object.entries(parentPackagePolicy.builtin) .filter(([, allowed]) => allowed === true) // eslint-disable-next-line @typescript-eslint/no-unused-vars .map(([packagePath, allowed]) => packagePath.split('.')[0]) // resolve the moduleId from the requestedName const moduleId = getRelativeModuleId(parentModuleId, requestedName) // browserify goop: // recursive requires dont hit cache so it inf loops, so we shortcircuit // this only seems to happen with a few browserify builtins (nodejs builtin module polyfills) // we could likely allow any requestedName since it can only refer to itself if (moduleId === parentModuleId) { if (['timers', 'buffer'].includes(requestedName) === false) { throw new Error(`LavaMoat - recursive require detected: "${requestedName}"`) } return parentModuleExports } // load module let moduleExports = internalRequire(moduleId) // look up config for module const moduleData = loadModuleData(moduleId) const packageName = moduleData.package // disallow requiring packages that are not in the parent's whitelist const isSamePackage = packageName === parentModulePackageName const parentIsEntryModule = parentModulePackageName === rootPackageName let isInParentWhitelist = false if (moduleData.type === 'builtin') { isInParentWhitelist = parentBuiltinsWhitelist.includes(packageName) } else { isInParentWhitelist = (parentPackagesWhitelist[packageName] === true) } // validate that the import is allowed if (!parentIsEntryModule && !isSamePackage && !isInParentWhitelist) { let typeText = ' ' if (moduleData.type === 'builtin') { typeText = ' node builtin ' } throw new Error(`LavaMoat - required${typeText}package not in allowlist: package "${parentModulePackageName}" requested "${packageName}" as "${requestedName}"`) } // create minimal selection if its a builtin and the whole path is not selected for if (!parentIsEntryModule && moduleData.type === 'builtin' && !parentPackagePolicy.builtin[moduleId]) { moduleExports = getBuiltinForConfig(moduleExports, moduleId, parentPackagePolicy.builtin) } return moduleExports } function prepareModuleInitializer (moduleData, packagePolicy) { const { moduleInitializer, precompiledInitializer, package: packageName, id: moduleId, source: moduleSource } = moduleData // moduleInitializer may be set by loadModuleData (e.g. builtin + native modules) if (moduleInitializer) { // if an external moduleInitializer is set, ensure it is allowed if (moduleData.type === 'native') { // ensure package is allowed to have native modules if (packagePolicy.native !== true) { throw new Error(`LavaMoat - "native" module type not permitted for package "${packageName}", module "${moduleId}"`) } } else if (moduleData.type !== 'builtin') { // builtin module types dont have policy configurations // but the packages that can import them are constrained elsewhere // here we just ensure that the module type is the only other type with a external moduleInitializer throw new Error(`LavaMoat - invalid external moduleInitializer for module type "${moduleData.type}" in package "${packageName}", module "${moduleId}"`) } // moduleObj must be from the same Realm as the moduleInitializer (eg dart2js runtime requirement) // here we are assuming the provided moduleInitializer is from the same Realm as this kernel const moduleObj = { exports: {} } return { moduleInitializer, moduleObj } } // setup initializer from moduleSource and compartment. // execute in package compartment with globalThis populated per package policy const packageCompartment = getCompartmentForPackage(packageName, packagePolicy) try { let moduleObj let moduleInitializer if (runWithPrecompiledModules) { if (!precompiledInitializer) { throw new Error(`LavaMoat - precompiledInitializer missing for "${moduleId}" from package "${packageName}"`) } // moduleObj must be from the same Realm as the moduleInitializer (eg dart2js runtime requirement) // here we are assuming the provided moduleInitializer is from the same Realm as this kernel moduleObj = { exports: {} } const evalKit = { globalThis: packageCompartment.globalThis, scopeTerminator: strictScopeTerminator, } // this invokes the with-proxy wrapper const moduleInitializerFactory = precompiledInitializer.call(evalKit) // this ensures strict mode moduleInitializer = moduleInitializerFactory() } else { if (typeof moduleSource !== 'string') { throw new Error(`LavaMoat - moduleSource not a string for "${moduleId}" from package "${packageName}"`) } const sourceURL = moduleData.file || `modules/${moduleId}` if (sourceURL.includes('\n')) { throw new Error(`LavaMoat - Newlines not allowed in filenames: ${JSON.stringify(sourceURL)}`) } // moduleObj must be from the same Realm as the moduleInitializer (eg dart2js runtime requirement) moduleObj = packageCompartment.evaluate('({ exports: {} })') // TODO: move all source mutations elsewhere moduleInitializer = packageCompartment.evaluate(`${moduleSource}\n//# sourceURL=${sourceURL}`) } return { moduleInitializer, moduleObj } } catch (err) { console.warn(`LavaMoat - Error evaluating module "${moduleId}" from package "${packageName}" \n${err.stack}`) throw err } } function createRootPackageCompartment (globalRef) { if (packageCompartmentCache.has(rootPackageName)) { throw new Error('LavaMoat - createRootPackageCompartment called more than once') } // prepare the root package's SES Compartment // endowments: // - Math is for untamed Math.random // - Date is for untamed Date.now const rootPackageCompartment = new Compartment({ Math, Date }) copyWrappedGlobals(globalRef, rootPackageCompartment.globalThis, globalThisRefs) // save the compartment for use by other modules in the package packageCompartmentCache.set(rootPackageName, rootPackageCompartment) return rootPackageCompartment } function getCompartmentForPackage (packageName, packagePolicy) { // compartment may have already been created let packageCompartment = packageCompartmentCache.get(packageName) if (packageCompartment) { return packageCompartment } // prepare Compartment if (getExternalCompartment && packagePolicy.env) { // external compartment can be provided by the platform (eg lavamoat-node) packageCompartment = getExternalCompartment(packageName, packagePolicy) } else { // prepare the module's SES Compartment // endowments: // - Math is for untamed Math.random // - Date is for untamed Date.now packageCompartment = new Compartment({ Math, Date }) } // prepare endowments let endowments try { endowments = getEndowmentsForConfig( // source reference rootPackageCompartment.globalThis, // policy packagePolicy, // unwrap to globalRef, // unwrap from packageCompartment.globalThis, ) } catch (err) { const errMsg = `Lavamoat - failed to prepare endowments for package "${packageName}":\n${err.stack}` throw new Error(errMsg) } // transform functions, getters & setters on prop descs. Solves SES scope proxy bug // WARNING: this part should be unnecessary since SES refactor into multiple nested with statements Object.entries(Object.getOwnPropertyDescriptors(endowments)) // ignore non-configurable properties because we are modifying endowments in place .filter(([, propDesc]) => propDesc.configurable) .forEach(([key, propDesc]) => { const wrappedPropDesc = applyEndowmentPropDescTransforms(propDesc, packageCompartment.globalThis, rootPackageCompartment.globalThis) Reflect.defineProperty(endowments, key, wrappedPropDesc) }) // sets up read/write access as configured const globalsConfig = packagePolicy.globals prepareCompartmentGlobalFromConfig(packageCompartment, globalsConfig, endowments, globalStore, globalThisRefs) // save the compartment for use by other modules in the package packageCompartmentCache.set(packageName, packageCompartment) return packageCompartment } // this gets the lavaMoat config for a module by packageName // if there were global defaults (e.g. everything gets "console") they could be applied here function getPolicyForPackage (config, packageName) { const packageConfig = (config.resources || {})[packageName] || {} packageConfig.globals = packageConfig.globals || {} packageConfig.packages = packageConfig.packages || {} packageConfig.builtin = packageConfig.builtin || {} return packageConfig } } })()