diff --git a/packages/webpack/README.md b/packages/webpack/README.md index a975237b01..fb4b1890e1 100644 --- a/packages/webpack/README.md +++ b/packages/webpack/README.md @@ -14,15 +14,20 @@ LavaMoat Webpack Plugin wraps each module in the bundle in a [Compartment](https > The plugin is emitting lockdown without the `.js` extension because that's the only way to prevent it from getting minified, which is likely to break it. -The LavaMoat plugin takes an options object with the following properties: - -- policy: the LavaMoat policy object. (unstable. This will surely change before v1 or a policy loader export will be provided from the main package to incorporate policy-override files) -- runChecks: Optional boolean property to indicate whether to check resulting code with wrapping for correctness. Default is false. -- diagnosticsVerbosity: Optional number property to represent diagnostics output verbosity. A larger number means more overwhelming diagnostics output. Default is 0. - Setting positive verbosity will enable runChecks. -- readableResourceIds: Decide whether to keep resource IDs human readable (regardless of production/development mode). If false, they are replaced with a sequence of numbers. Keeping them readable may be useful for debugging when a policy violation error is thrown. -- lockdown: set configuration for [SES lockdown](). Setting the option replaces defaults from LavaMoat. -- HtmlWebpackPluginInterop: add a script tag to the html output for ./lockdown file if HtmlWebpackPlugin is in use +The LavaMoat plugin takes an options object with the following properties (all optional): + +| Property | Description | Default | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| `policyLocation` | Directory to store policy files in. | `./lavamoat/webpack` | +| `generatePolicy` | Whether to generate the `policy.json` file. Generated policy is used in the build immediately. `policy-override.json` is applied before bundling, if present. | `false` | +| `emitPolicySnapshot` | If enabled, emits the result of merging policy with overrides into the output directory of Webpack build for inspection. The file is not used by the bundle. | `false` | +| `readableResourceIds` | Boolean to decide whether to keep resource IDs human readable (possibly regardless of production/development mode). If `false`, they are replaced with a sequence of numbers. Keeping them readable may be useful for debugging when a policy violation error is thrown. By default, follows the Webpack config mode. | `(mode==='development')` | +| `lockdown` | Configuration for [SES lockdown][]. Setting the option replaces defaults from LavaMoat. | reasonable defaults | +| `HtmlWebpackPluginInterop` | Boolean to add a script tag to the HTML output for `./lockdown` file if `HtmlWebpackPlugin` is in use. | `false` | +| `inlineLockdown` | Array of output filenames in which to inline lockdown (instead of adding it as a file to the output directory). | +| `runChecks` | Boolean property to indicate whether to check resulting code with wrapping for correctness. | `false` | +| `diagnosticsVerbosity` | Number property to represent diagnostics output verbosity. A larger number means more overwhelming diagnostics output. Setting a positive verbosity will enable `runChecks`. | `0` | +| `policy` | The LavaMoat policy object (if not loading from file; see `policyLocation`) | `undefined` | ```js const LavaMoatPlugin = require('@lavamoat/webpack') @@ -31,18 +36,8 @@ module.exports = { // ... other webpack configuration properties plugins: [ new LavaMoatPlugin({ - // policy generated by lavamoat - policy: require('./lavamoat/policy.json'), - // runChecks: true, // enables checking each wrapped module source if it's still proper JavaScript (in case mismatching braces somehow survived Webpack loaders processing) - // readableResourceIds: true, // explicitly decide if resourceIds from policy should be readable in the bundle or turned into numbers. You might want to bundle in production mode but keep the ids for debugging - // diagnosticsVerbosity: 2, // level of output verbosity from the plugin - // SES lockdown options to use at runtime - // lockdown: { - // errorTaming: "unsafe", - // consoleTaming: "unsafe", - // overrideTaming: "severe" - // }, - // HtmlWebpackPluginInterop: false, // set it to true if you want a script tag for ./lockdown file to automatically be added to your HTML template + generatePolicy: true, + // ... settings from above, optionally }), ], // ... other webpack configuration properties @@ -74,7 +69,7 @@ Example: avoid wrapping CSS modules: 'css-loader', LavaMoatPlugin.exclude, ], - sideEffects: true, + // ... }, ], }, @@ -107,12 +102,13 @@ Sadly, even treeshaking doesn't eliminate that module. It's left there and faili This plugin will skip policy enforcement for such ignored modules. -# Security Claims +# Security -**This is a _beta_ release and does not provide any guarantees; even those listed below. Use at your own risk!** +**This is an experimental software. Use at your own risk!** -- SES must be added to the page without any bundling or transforming for any security guarantees to be sustained. - - The plugin is attempting to add it as an asset to the compilation for the sake of Developer Experience. Feedback welcome. +- [SES lockdown][] must be added to the page without any bundling or transforming for any security guarantees to be sustained. + - The plugin is attempting to add it as an asset to the compilation for the sake of Developer Experience. `.js` extension is omitted to prevent minification. + - Optionally lockdown can be inlined into the bundle files. You need to list the scripts that get to load as the first script on the page to apply lockdown only once when inlined. When you have a single bundle, you just configure a list with one element. It gets more complex with builds for multiple pages. The plugin doesn't attempt to guess where to inline lockdown. - Each javascript module resulting from the webpack build is scoped to its package's policy ## Threat Model @@ -137,3 +133,5 @@ Run `npm test` to start the automated tests. - Navigate to `example/` - Run `npm ci` and `npm test` - Open `dist/index.html` in your browser and inspect the console + +[SES lockdown]: https://github.com/endojs/endo/tree/master/packages/ses#lockdown diff --git a/packages/webpack/src/buildtime/emitSes.js b/packages/webpack/src/buildtime/emitSes.js index 43e0190798..3cd0d5b68d 100644 --- a/packages/webpack/src/buildtime/emitSes.js +++ b/packages/webpack/src/buildtime/emitSes.js @@ -1,40 +1,79 @@ const { readFileSync } = require('node:fs') +const path = require('node:path') const { - sources: { RawSource }, + sources: { RawSource, ConcatSource }, } = require('webpack') +const lockdownSource = readFileSync( + path.join(require.resolve('ses'), '../lockdown.umd.min.js'), + 'utf-8' +) +const lockdownSourcePrefix = `;(function(){\n${lockdownSource}\n})();\n` + module.exports = { + /** + * @param {object} options + * @param {import('webpack').Compilation} options.compilation + * @param {string[]} options.inlineLockdown + * @returns {() => void} + */ + sesPrefixFiles: + ({ compilation, inlineLockdown: files }) => + () => { + files.forEach((file) => { + const asset = compilation.assets[file] + if (!asset) { + throw new Error( + `LavaMoatPlugin: file specified in inlineLockdown not found in compilation: ${file}` + ) + } + compilation.assets[file] = new ConcatSource(lockdownSourcePrefix, asset) + }) + }, + /** + * @param {object} options + * @param {import('webpack').Compilation} options.compilation + * @param {import('webpack').WebpackPluginInstance} [options.HtmlWebpackPluginInUse] + * @param {boolean} [options.HtmlWebpackPluginInterop] + * @returns {() => void} + */ sesEmitHook: ({ compilation, HtmlWebpackPluginInUse, HtmlWebpackPluginInterop }) => () => { - const sesFile = readFileSync(require.resolve('ses'), 'utf-8') // TODO: to consider: instead manually copy to compiler.options.output.path - const asset = new RawSource(sesFile) + const asset = new RawSource(lockdownSource) compilation.emitAsset('lockdown', asset) if (HtmlWebpackPluginInUse && HtmlWebpackPluginInterop) { HtmlWebpackPluginInUse.constructor + // @ts-expect-error - incomplete types .getHooks(compilation) - .beforeEmit.tapAsync('LavaMoatWebpackPlugin-lockdown', (data, cb) => { - const scriptTag = '' - const headTagRegex = /]*>/iu - const scriptTagRegex = /' + const headTagRegex = /]*>/iu + const scriptTagRegex = /