Skip to content

Commit

Permalink
feat(webpack): ability to inline lockdown
Browse files Browse the repository at this point in the history
  • Loading branch information
naugtur authored and legobeat committed Mar 14, 2024
1 parent 816e7b7 commit e4b1d7a
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 43 deletions.
79 changes: 59 additions & 20 deletions packages/webpack/src/buildtime/emitSes.js
Original file line number Diff line number Diff line change
@@ -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 = '<script src="./lockdown"></script>'
const headTagRegex = /<head[^>]*>/iu
const scriptTagRegex = /<script/iu
.beforeEmit.tapAsync(
'LavaMoatWebpackPlugin-lockdown',
(
/** @type {{ html: string }} */ data,
/** @type {(arg0: null, arg1: any) => void} */ cb
) => {
const scriptTag = '<script src="./lockdown"></script>'
const headTagRegex = /<head[^>]*>/iu
const scriptTagRegex = /<script/iu

if (headTagRegex.test(data.html)) {
data.html = data.html.replace(headTagRegex, `$&${scriptTag}`)
} else if (scriptTagRegex.test(data.html)) {
data.html = data.html.replace(
scriptTagRegex,
`${scriptTag}<script`
)
} else {
throw Error(
'LavaMoat: Could not insert lockdown script tag, no suitable location found in the html template'
)
if (headTagRegex.test(data.html)) {
data.html = data.html.replace(headTagRegex, `$&${scriptTag}`)
} else if (scriptTagRegex.test(data.html)) {
data.html = data.html.replace(
scriptTagRegex,
`${scriptTag}<script`
)
} else {
throw Error(
'LavaMoat: Could not insert lockdown script tag, no suitable location found in the html template'
)
}
cb(null, data)
}
cb(null, data)
})
)
}
},
}
66 changes: 43 additions & 23 deletions packages/webpack/src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const JAVASCRIPT_MODULE_TYPE_ESM = 'javascript/esm'
// @ts-ignore
const { RUNTIME_KEY } = require('./ENUM.json')
const { wrapGeneratorMaker } = require('./buildtime/generator.js')
const { sesEmitHook } = require('./buildtime/emitSes.js')
const { sesEmitHook, sesPrefixFiles } = require('./buildtime/emitSes.js')
const EXCLUDE_LOADER = path.join(__dirname, './excludeLoader.js')

class VirtualRuntimeModule extends RuntimeModule {
Expand Down Expand Up @@ -63,13 +63,17 @@ class LavaMoatPlugin {
* @param {import('./types.js').LavaMoatPluginOptions} [options]
*/
constructor(options = {}) {
if (!options.lockdown) {
options.lockdown = lockdownDefaults
/**
* @type {import('./types.js').LavaMoatPluginOptions & {
* policyLocation: string
* lockdown: object
* }}
*/
this.options = {
lockdown: lockdownDefaults,
policyLocation: path.join('.', 'lavamoat', 'webpack'),
...options,
}
if (!options.policyLocation) {
options.policyLocation = path.join('.', 'lavamoat', 'webpack')
}
this.options = options

diag.level = options.diagnosticsVerbosity || 0
}
Expand Down Expand Up @@ -429,22 +433,38 @@ class LavaMoatPlugin {
}
)

const HtmlWebpackPluginInUse = compiler.options.plugins.find(
(plugin) => plugin && plugin.constructor.name === 'HtmlWebpackPlugin'
)

// TODO: avoid minification
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
sesEmitHook({
compilation,
HtmlWebpackPluginInUse,
HtmlWebpackPluginInterop: options.HtmlWebpackPluginInterop,
})
)
if (options.inlineLockdown) {
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
},
sesPrefixFiles({
compilation,
inlineLockdown: options.inlineLockdown,
})
)
} else {
const HtmlWebpackPluginInUse = compiler.options.plugins.find(
/**
* @param {unknown} plugin
* @returns {plugin is import('webpack').WebpackPluginInstance}
*/
(plugin) =>
!!plugin && plugin.constructor.name === 'HtmlWebpackPlugin'
)
compilation.hooks.processAssets.tap(
{
name: PLUGIN_NAME,
stage: Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
sesEmitHook({
compilation,
HtmlWebpackPluginInUse,
HtmlWebpackPluginInterop: !!options.HtmlWebpackPluginInterop,
})
)
}

// TODO: add later hooks to optionally verify correctness and totality
// of wrapping for the paranoid mode.
Expand Down
2 changes: 2 additions & 0 deletions packages/webpack/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
* @property {Policy} [policy] - LavaMoat policy object - if programmaticly
* created
* @property {Object} [lockdown] - Options to pass to lockdown
* @property {string[]} [inlineLockdown] - Prefix the listed files with lockdown
* code
*/

/**
Expand Down
29 changes: 29 additions & 0 deletions packages/webpack/test/e2e-lockdown-inline.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const test = require('ava')
const { scaffold, runScript } = require('./scaffold.js')
const { makeConfig } = require('./fixtures/main/webpack.config.js')

test.before(async (t) => {
const webpackConfig = makeConfig({
inlineLockdown: ['app.js'],
})
await t.notThrowsAsync(async () => {
t.context.build = await scaffold(webpackConfig)
}, 'Expected the build to succeed')
t.context.bundle = t.context.build.snapshot['/dist/app.js']
})

test('webpack/lockdown-inline - bundle runs under lockdown', (t) => {
t.notThrows(() => {
runScript(
t.context.bundle +
`
;;
if (!Object.isFrozen(Object.prototype)) {
throw Error('expected Object.prototype to be frozen');
}
if (!Compartment || !harden) {
throw Error('expected SES globals to be available');
}`
)
})
})

0 comments on commit e4b1d7a

Please sign in to comment.