@lavamoat/webpack
Version:
LavaMoat Webpack plugin for running dependencies in Compartments without eval
419 lines (392 loc) • 13.4 kB
JavaScript
const { RUNTIME_KEY } = require('../ENUM.json')
const diag = require('../buildtime/diagnostics.js')
const { assembleRuntime, prepareSource } = require('./assemble.js')
const { adjustScuttleConfig } = require('./scuttlingConf.js')
const path = require('node:path')
const { RuntimeModule } = require('webpack')
class VirtualRuntimeModule extends RuntimeModule {
/**
* @param {Object} options The options for the VirtualRuntimeModule.
* @param {string} options.name The name of the module.
* @param {string} options.source The source code of the module.
* @param {number} [options.stage] The stage of runtime. One of
* RuntimeModule.STAGE_*.
* @param {boolean} [options.withoutClosure] Make the source code run outside
* the closure for a runtime module
*/
constructor({
name,
source,
stage = RuntimeModule.STAGE_NORMAL,
withoutClosure = false,
}) {
super(name, stage)
this.withoutClosure = withoutClosure
this.virtualSource = `;${source};`
}
shouldIsolate() {
return !this.withoutClosure
}
/**
* Returns the virtual source code.
*
* @returns {string} Virtual source code string
* @override
*/
generate() {
return this.virtualSource
}
}
/** @import {LavaMoatPluginOptions, LavaMoatChunkRuntimeConfiguration} from '../buildtime/types' */
/** @import {LavaMoatPolicy} from '@lavamoat/types' */
/** @import {RuntimeFragment} from './assemble.js' */
/** @import {Chunk} from 'webpack' */
/** @import {ProgressAPI} from '../buildtime/utils.js' */
/**
* @typedef {Object} LavaMoatRuntimeIdentifiers
* @property {string} root Root identifier
* @property {[string, (string | number)[]][]} identifiersForModuleIds Module ID
* to identifier mappings
* @property {(string | number)[]} unenforceableModuleIds IDs of modules that
* cannot be enforced
* @property {(string | number)[]} [contextModuleIds] Context module IDs
* @property {Record<string | number, string>} [externals] External module
* configurations
*/
module.exports = {
/**
* Builds the LavaMoat runtime configuration and generates runtime source code
*
* @param {Object} params The parameters object
* @param {LavaMoatPluginOptions} params.options Runtime configuration options
*/
runtimeBuilder({ options }) {
/**
* Generates the preamble that caches selected globals to protect runtime
* from scuttling. The set of cached globals is limited to ones known to be
* used by runtime modules for the sake of bundle size.
*
* @returns {string}
*/
function getDefensiveCodingPreamble() {
const globals = [
'location', // AutoPublicPathRuntimeModule.js
'setTimeout', // LoadScriptRuntimeModule.js
'clearTimeout', // LoadScriptRuntimeModule.js
'document', // LoadScriptRuntimeModule.js, AutoPublicPathRuntimeModule.js
'trustedTypes', // GetTrustedTypesPolicyRuntimeModule.js
'self',
]
return `var ${globals.map((g) => `${g} = globalThis.${g}`).join(',')};
const LOCKDOWN_SHIMS = [];`
}
/**
* Prepares the static shims to be included in the runtime chunk.
*
* @param {string[]} dependencies The module specifiers to prepare.
* @returns {string} The prepared runtime dependencies source.
*/
function getStaticShims(dependencies) {
/**
* Wraps static shim source code to capture the lockdown shim it sets (if
* any) Makes LOCKDOWN_SHIMS unreachable in its scope and freezes so no
* other runtime modules can add any new shims to it.
*
* @param {string} source The source code to wrap.
* @returns {string} The wrapped source code.
*/
const shimWrap = (source) => `
LOCKDOWN_SHIMS.push((LOCKDOWN_SHIMS)=>{
${source}
;
})`
const shims = dependencies.map((dep) => {
const source = prepareSource(dep)
try {
new Function(source)
} catch (e) {
throw new Error(
`LavaMoatPlugin: Static shim ${dep} is not valid JS`,
{ cause: e }
)
}
return shimWrap(source)
})
return `${shims.join('\n')}
Object.freeze(LOCKDOWN_SHIMS);`
}
function getUnlockedRuntime() {
/** @satisfies {readonly RuntimeFragment[]} */
const runtimeFragments = /** @type {const} */ ([
{
name: 'ENUM',
file: require.resolve('../ENUM.json'),
json: true,
},
{
name: 'runtime',
file: require.resolve('./runtimeUnlocked.js'),
},
])
const unlockedRuntime = assembleRuntime(RUNTIME_KEY, runtimeFragments)
return unlockedRuntime
}
/**
* Generates the LavaMoat runtime source code based on chunk configuration
*
* @param {Object} params The parameters object
* @param {LavaMoatChunkRuntimeConfiguration['embeddedOptions']} params.embeddedOptions
* The options to embed in the runtime (a selection of plugin options)
* @param {(string | number)[]} params.chunkIds Array of chunk identifiers
* @param {LavaMoatPolicy} params.policyData LavaMoat security policy
* configuration
* @param {LavaMoatRuntimeIdentifiers} params.identifiers Object containing
* module identifier mappings
* @returns {string} The assembled runtime source code
*/
function getLavaMoatRuntimeSource({
embeddedOptions,
chunkIds,
policyData,
identifiers: {
root,
identifiersForModuleIds,
unenforceableModuleIds,
contextModuleIds,
externals,
},
}) {
let repairs
if (options.skipRepairs === true) {
repairs = ''
} else {
repairs = require('./repairsBuilder.js').buildRepairs(
policyData,
options.skipRepairs
)
}
/** @satisfies {readonly RuntimeFragment[]} */
const runtimeFragments = /** @type {const} */ ([
// the string used to indicate root resource id
{
name: 'root',
data: root,
json: true,
},
// a mapping used to look up resource ids by module id
{
name: 'idmap',
data: identifiersForModuleIds,
json: true,
},
// list of ids of modules to skip in policy enforcement
{
name: 'unenforceable',
data: unenforceableModuleIds,
json: true,
},
// list of known context modules
{
name: 'ctxm',
data: contextModuleIds || null,
json: true,
},
// known chunk ids
{
name: 'kch',
data: chunkIds,
json: true,
},
// a record of module ids that are externals and need to be enforced as builtins
{
name: 'externals',
data: externals || null,
json: true,
},
// options to turn on scuttling
{ name: 'options', data: embeddedOptions, json: true },
// scuttling module, if needed
...(embeddedOptions?.scuttleGlobalThis?.enabled === true
? [
{
name: 'scuttling',
shimRequire: 'lavamoat-core/src/scuttle.js',
},
]
: []),
// the policy itself
{ name: 'policy', data: policyData, json: true },
// enum for keys to match the generated ones in wrapper
{
name: 'ENUM',
file: require.resolve('../ENUM.json'),
json: true,
},
// endowments module
{
name: 'endowmentsToolkit',
shimRequire: 'lavamoat-core/src/endowmentsToolkit.js',
},
// repairs
{
name: 'repairs',
rawSource: repairs,
},
// main lavamoat runtime
{
name: 'runtime',
file: require.resolve('./runtime.js'),
},
// Optional debug helpers
...(options.debugRuntime
? [
{
name: 'debug',
shimRequire: path.join(__dirname, 'debug.js'),
},
]
: []),
])
const lavaMoatRuntime = assembleRuntime(RUNTIME_KEY, runtimeFragments)
const size = lavaMoatRuntime.length / 1024
diag.rawDebug(
2,
`Total LavaMoat runtime and policy data size: ${size.toFixed(0)}KB (before minification)`
)
return lavaMoatRuntime
}
return {
/**
* Generates the LavaMoat runtime source code based on chunk configuration
*
* @param {Object} params The parameters object
* @param {ProgressAPI} params.PROGRESS
* @param {Chunk} params.currentChunk The webpack chunk
* @param {(string | number)[]} params.chunkIds Array of chunk identifiers
* @param {LavaMoatPolicy} params.policyData LavaMoat security policy
* configuration
* @param {LavaMoatRuntimeIdentifiers} params.identifiers Object
* containing module identifier mappings
* @param {string} params.chunkLoaderName The name of the global that
* loads chunks
* @returns {VirtualRuntimeModule[]} The assembled runtime source code
*/
getLavaMoatRuntimeModules({
PROGRESS,
currentChunk,
chunkIds,
policyData,
identifiers,
chunkLoaderName,
}) {
const currentChunkName = currentChunk.name
const lavamoatRuntimeModules = [
new VirtualRuntimeModule({
name: 'LavaMoat/defensive',
source: getDefensiveCodingPreamble(),
stage: RuntimeModule.STAGE_NORMAL, // before all other runtime modules
withoutClosure: true, // run in the scope of the runtime closure
}),
]
/** @type {LavaMoatChunkRuntimeConfiguration} */
let runtimeConfiguration = {
mode: 'safe',
staticShims: options.staticShims_experimental,
embeddedOptions: {
scuttleGlobalThis: options.scuttleGlobalThis,
lockdown: options.lockdown,
},
}
// plugin options decide the mode
if (
options.unlockedChunksUnsafe &&
currentChunkName &&
options.unlockedChunksUnsafe.test(currentChunkName)
) {
runtimeConfiguration = {
mode: 'unlocked_unsafe',
}
}
if (options.runtimeConfigurationPerChunk_experimental) {
const chunkConfig =
options.runtimeConfigurationPerChunk_experimental(currentChunk)
if (chunkConfig) {
runtimeConfiguration = {
mode: chunkConfig.mode || runtimeConfiguration.mode,
staticShims:
chunkConfig.staticShims || runtimeConfiguration.staticShims,
embeddedOptions: Object.assign(
{},
runtimeConfiguration.embeddedOptions,
chunkConfig.embeddedOptions
),
}
}
}
if (runtimeConfiguration.embeddedOptions?.scuttleGlobalThis) {
runtimeConfiguration.embeddedOptions.scuttleGlobalThis =
adjustScuttleConfig(
runtimeConfiguration.embeddedOptions.scuttleGlobalThis,
{
chunkLoaderName,
}
)
}
// flesh out the modules for runtimeConfiguration
switch (runtimeConfiguration.mode) {
case 'unlocked_unsafe':
diag.rawDebug(
1,
`adding UNLOCKED runtime for chunk ${currentChunkName}`
)
lavamoatRuntimeModules.push(
new VirtualRuntimeModule({
name: 'LavaMoat/runtime',
source: getUnlockedRuntime(),
stage: RuntimeModule.STAGE_TRIGGER, // after all other stages
})
)
break
case 'null_unsafe':
diag.rawDebug(
1,
`Skipped adding runtime for chunk ${currentChunkName}`
)
break
case 'safe':
diag.rawDebug(2, `adding runtime for chunk ${currentChunkName}`)
lavamoatRuntimeModules.push(
new VirtualRuntimeModule({
name: 'LavaMoat/runtime',
source: getLavaMoatRuntimeSource({
chunkIds,
policyData,
identifiers,
embeddedOptions: runtimeConfiguration.embeddedOptions,
}),
stage: RuntimeModule.STAGE_TRIGGER, // after all other stages
})
)
PROGRESS.report('runtimeAdded')
break
}
if (
runtimeConfiguration.staticShims &&
Array.isArray(runtimeConfiguration.staticShims)
) {
const staticShimsWrapped = getStaticShims(
runtimeConfiguration.staticShims
)
lavamoatRuntimeModules.push(
new VirtualRuntimeModule({
name: 'LavaMoat/staticShims',
source: staticShimsWrapped,
stage: RuntimeModule.STAGE_BASIC, // after Normal
})
)
}
return lavamoatRuntimeModules
},
}
},
}