mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Activate LavaMoat scuttling security feature (#17276)
This commit is contained in:
parent
f5426a84d9
commit
4a57d994c7
@ -8,7 +8,7 @@ try {
|
|||||||
* universalPropertyNames constant specified in 'ses/src/whitelist'. This
|
* universalPropertyNames constant specified in 'ses/src/whitelist'. This
|
||||||
* function makes all function and object properties on the start compartment
|
* function makes all function and object properties on the start compartment
|
||||||
* global non-configurable and non-writable, unless they are already
|
* global non-configurable and non-writable, unless they are already
|
||||||
* non-configurable.
|
* non-configurable, or they were scuttled by LavaMoat runtime (LavaMoat#360).
|
||||||
*
|
*
|
||||||
* It is critical that this function runs at the right time during
|
* It is critical that this function runs at the right time during
|
||||||
* initialization, which should always be immediately after `lockdown` has been
|
* initialization, which should always be immediately after `lockdown` has been
|
||||||
@ -22,6 +22,9 @@ try {
|
|||||||
* We write this function in IIFE format to avoid polluting global scope.
|
* We write this function in IIFE format to avoid polluting global scope.
|
||||||
*/
|
*/
|
||||||
(function protectIntrinsics() {
|
(function protectIntrinsics() {
|
||||||
|
const lmre = // regex expression for LavaMoat scuttling error message
|
||||||
|
/LavaMoat - property "[A-Za-z0-9]*" of globalThis is inaccessible under scuttling mode/u;
|
||||||
|
|
||||||
const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis);
|
const namedIntrinsics = Reflect.ownKeys(new Compartment().globalThis);
|
||||||
|
|
||||||
// These named intrinsics are not automatically hardened by `lockdown`
|
// These named intrinsics are not automatically hardened by `lockdown`
|
||||||
@ -62,7 +65,18 @@ try {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shouldHardenManually.has(propertyName)) {
|
if (shouldHardenManually.has(propertyName)) {
|
||||||
harden(globalThis[propertyName]);
|
try {
|
||||||
|
harden(globalThis[propertyName]);
|
||||||
|
} catch (err) {
|
||||||
|
if (!lmre.test(err.message)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
`Property ${propertyName} will not be hardened`,
|
||||||
|
`because it is scuttled by LavaMoat protection.`,
|
||||||
|
`Visit https://github.com/LavaMoat/LavaMoat/pull/360 to learn more.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -74,7 +74,52 @@ async function defineAndRunBuildTasks() {
|
|||||||
} = await parseArgv();
|
} = await parseArgv();
|
||||||
|
|
||||||
// build lavamoat runtime file
|
// build lavamoat runtime file
|
||||||
await lavapack.buildRuntime({ scuttleGlobalThis: false });
|
// build lavamoat runtime file
|
||||||
|
await lavapack.buildRuntime({
|
||||||
|
scuttleGlobalThis: true,
|
||||||
|
scuttleGlobalThisExceptions: [
|
||||||
|
// globals used by different mm deps outside of lm compartment
|
||||||
|
'toString',
|
||||||
|
'getComputedStyle',
|
||||||
|
'addEventListener',
|
||||||
|
'removeEventListener',
|
||||||
|
'ShadowRoot',
|
||||||
|
'HTMLElement',
|
||||||
|
'Element',
|
||||||
|
'pageXOffset',
|
||||||
|
'pageYOffset',
|
||||||
|
'visualViewport',
|
||||||
|
'Reflect',
|
||||||
|
'Set',
|
||||||
|
'Object',
|
||||||
|
'navigator',
|
||||||
|
'harden',
|
||||||
|
'console',
|
||||||
|
// globals chrome driver needs to function (test env)
|
||||||
|
/cdc_[a-zA-Z0-9]+_[a-zA-Z]+/iu,
|
||||||
|
'performance',
|
||||||
|
'parseFloat',
|
||||||
|
'innerWidth',
|
||||||
|
'innerHeight',
|
||||||
|
'Symbol',
|
||||||
|
'Math',
|
||||||
|
'DOMRect',
|
||||||
|
'Number',
|
||||||
|
'Array',
|
||||||
|
'crypto',
|
||||||
|
'Function',
|
||||||
|
'Uint8Array',
|
||||||
|
'String',
|
||||||
|
'Promise',
|
||||||
|
// globals sentry needs to function
|
||||||
|
'__SENTRY__',
|
||||||
|
'appState',
|
||||||
|
'extra',
|
||||||
|
'stateHooks',
|
||||||
|
'sentryHooks',
|
||||||
|
'sentry',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const browserPlatforms = ['firefox', 'chrome'];
|
const browserPlatforms = ['firefox', 'chrome'];
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ const {
|
|||||||
isTestBuild,
|
isTestBuild,
|
||||||
getEnvironment,
|
getEnvironment,
|
||||||
logError,
|
logError,
|
||||||
|
wrapAgainstScuttling,
|
||||||
} = require('./utils');
|
} = require('./utils');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -50,6 +51,42 @@ const {
|
|||||||
createRemoveFencedCodeTransform,
|
createRemoveFencedCodeTransform,
|
||||||
} = require('./transforms/remove-fenced-code');
|
} = require('./transforms/remove-fenced-code');
|
||||||
|
|
||||||
|
// map dist files to bag of needed native APIs against LM scuttling
|
||||||
|
const scuttlingConfig = {
|
||||||
|
'sentry-install.js': {
|
||||||
|
// globals sentry need to function
|
||||||
|
window: '',
|
||||||
|
navigator: '',
|
||||||
|
location: '',
|
||||||
|
Uint16Array: '',
|
||||||
|
fetch: '',
|
||||||
|
String: '',
|
||||||
|
Math: '',
|
||||||
|
Object: '',
|
||||||
|
Symbol: '',
|
||||||
|
Function: '',
|
||||||
|
Array: '',
|
||||||
|
Boolean: '',
|
||||||
|
Number: '',
|
||||||
|
Request: '',
|
||||||
|
Date: '',
|
||||||
|
document: '',
|
||||||
|
JSON: '',
|
||||||
|
encodeURIComponent: '',
|
||||||
|
crypto: '',
|
||||||
|
// {clear/set}Timeout are "this sensitive"
|
||||||
|
clearTimeout: 'window',
|
||||||
|
setTimeout: 'window',
|
||||||
|
// sentry special props
|
||||||
|
__SENTRY__: '',
|
||||||
|
sentryHooks: '',
|
||||||
|
sentry: '',
|
||||||
|
appState: '',
|
||||||
|
extra: '',
|
||||||
|
stateHooks: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the appropriate Infura project ID.
|
* Get the appropriate Infura project ID.
|
||||||
*
|
*
|
||||||
@ -320,6 +357,7 @@ function createScriptTasks({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,6 +381,7 @@ function createScriptTasks({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,6 +409,7 @@ function createScriptTasks({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
}),
|
}),
|
||||||
createNormalBundle({
|
createNormalBundle({
|
||||||
buildTarget,
|
buildTarget,
|
||||||
@ -382,6 +422,7 @@ function createScriptTasks({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -456,6 +497,7 @@ async function createManifestV3AppInitializationBundle({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Code below is used to set statsMode to true when testing in MV3
|
// Code below is used to set statsMode to true when testing in MV3
|
||||||
@ -721,6 +763,7 @@ function createFactoredBuild({
|
|||||||
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
* @param {boolean} options.shouldLintFenceFiles - Whether files with code
|
||||||
* fences should be linted after fences have been removed.
|
* fences should be linted after fences have been removed.
|
||||||
* @param {string} options.version - The current version of the extension.
|
* @param {string} options.version - The current version of the extension.
|
||||||
|
* @param {boolean} options.applyLavaMoat - Whether to apply LavaMoat or not
|
||||||
* @returns {Function} A function that creates the bundle.
|
* @returns {Function} A function that creates the bundle.
|
||||||
*/
|
*/
|
||||||
function createNormalBundle({
|
function createNormalBundle({
|
||||||
@ -735,6 +778,7 @@ function createNormalBundle({
|
|||||||
policyOnly,
|
policyOnly,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
version,
|
version,
|
||||||
|
applyLavaMoat,
|
||||||
}) {
|
}) {
|
||||||
return async function () {
|
return async function () {
|
||||||
// create bundler setup and apply defaults
|
// create bundler setup and apply defaults
|
||||||
@ -763,6 +807,7 @@ function createNormalBundle({
|
|||||||
minify,
|
minify,
|
||||||
reloadOnChange,
|
reloadOnChange,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
|
applyLavaMoat,
|
||||||
});
|
});
|
||||||
|
|
||||||
// set bundle entries
|
// set bundle entries
|
||||||
@ -812,6 +857,7 @@ function setupBundlerDefaults(
|
|||||||
minify,
|
minify,
|
||||||
reloadOnChange,
|
reloadOnChange,
|
||||||
shouldLintFenceFiles,
|
shouldLintFenceFiles,
|
||||||
|
applyLavaMoat,
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const { bundlerOpts } = buildConfiguration;
|
const { bundlerOpts } = buildConfiguration;
|
||||||
@ -878,6 +924,9 @@ function setupBundlerDefaults(
|
|||||||
|
|
||||||
// Setup source maps
|
// Setup source maps
|
||||||
setupSourcemaps(buildConfiguration, { buildTarget });
|
setupSourcemaps(buildConfiguration, { buildTarget });
|
||||||
|
|
||||||
|
// Setup wrapping of code against scuttling (before sourcemaps generation)
|
||||||
|
setupScuttlingWrapping(buildConfiguration, applyLavaMoat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -931,6 +980,27 @@ function setupMinification(buildConfiguration) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setupScuttlingWrapping(buildConfiguration, applyLavaMoat) {
|
||||||
|
const { events } = buildConfiguration;
|
||||||
|
events.on('configurePipeline', ({ pipeline }) => {
|
||||||
|
pipeline.get('scuttle').push(
|
||||||
|
through.obj(
|
||||||
|
callbackify(async (file, _enc) => {
|
||||||
|
const configForFile = scuttlingConfig[file.relative];
|
||||||
|
if (applyLavaMoat && configForFile) {
|
||||||
|
const wrapped = wrapAgainstScuttling(
|
||||||
|
file.contents.toString(),
|
||||||
|
configForFile,
|
||||||
|
);
|
||||||
|
file.contents = Buffer.from(wrapped, 'utf8');
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setupSourcemaps(buildConfiguration, { buildTarget }) {
|
function setupSourcemaps(buildConfiguration, { buildTarget }) {
|
||||||
const { events } = buildConfiguration;
|
const { events } = buildConfiguration;
|
||||||
events.on('configurePipeline', ({ pipeline }) => {
|
events.on('configurePipeline', ({ pipeline }) => {
|
||||||
@ -976,6 +1046,8 @@ async function createBundle(buildConfiguration, { reloadOnChange }) {
|
|||||||
[],
|
[],
|
||||||
'vinyl',
|
'vinyl',
|
||||||
[],
|
[],
|
||||||
|
'scuttle',
|
||||||
|
[],
|
||||||
'sourcemaps:init',
|
'sourcemaps:init',
|
||||||
[],
|
[],
|
||||||
'minify',
|
'minify',
|
||||||
|
@ -118,6 +118,80 @@ function logError(error) {
|
|||||||
console.error(error.stack || error);
|
console.error(error.stack || error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function wrapAgainstScuttling() tries to generically wrap given code
|
||||||
|
* with an environment that allows it to still function under a scuttled environment.
|
||||||
|
*
|
||||||
|
* It's only (current) use is for sentry code which runs before scuttling happens but
|
||||||
|
* later on still leans on properties of the global object which at that point are scuttled.
|
||||||
|
*
|
||||||
|
* To accomplish that, we wrap the entire provided code with the good old with-proxy trick,
|
||||||
|
* which helps us capture access attempts like (1) window.fetch/globalThis.fetch and (2) fetch.
|
||||||
|
*
|
||||||
|
* wrapAgainstScuttling() function also accepts a bag of the global object's properties the
|
||||||
|
* code needs in order to properly function, and within our proxy we make sure to
|
||||||
|
* return those whenever the code goes through our proxy asking for them.
|
||||||
|
*
|
||||||
|
* Specifically when the code tries to set properties to the global object,
|
||||||
|
* in addition to the preconfigured properties, we also accept any property
|
||||||
|
* starting with on to support global event handlers settings.
|
||||||
|
*
|
||||||
|
* Also, sentry invokes functions dynamically using Function.prototype's call and apply,
|
||||||
|
* and our proxy messes with their this when that happens, so these two required a tailor-made patch.
|
||||||
|
*
|
||||||
|
* @param content - contents of the js code to wrap
|
||||||
|
* @param bag - bag of global object properties to provide to the wrapped js code
|
||||||
|
* @returns {string} wrapped js code
|
||||||
|
*/
|
||||||
|
function wrapAgainstScuttling(content, bag = {}) {
|
||||||
|
return `
|
||||||
|
{
|
||||||
|
function setupProxy(global) {
|
||||||
|
// bag of properties to allow vetted shim to access,
|
||||||
|
// mapped to their correct this value if needed
|
||||||
|
const bag = ${JSON.stringify(bag)};
|
||||||
|
// setup vetted shim bag of properties
|
||||||
|
for (const prop in bag) {
|
||||||
|
const that = bag[prop];
|
||||||
|
let api = global[prop];
|
||||||
|
if (that) api = api.bind(global[that]);
|
||||||
|
bag[prop] = api;
|
||||||
|
}
|
||||||
|
// setup proxy for the vetted shim to go through
|
||||||
|
const proxy = new Proxy(bag, {
|
||||||
|
set: function set(target, prop, value) {
|
||||||
|
if (bag.hasOwnProperty(prop) || prop.startsWith('on')) {
|
||||||
|
return bag[prop] = global[prop] = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// make sure bind() and apply() are applied with
|
||||||
|
// proxy target rather than proxy receiver
|
||||||
|
(function(target, receiver) {
|
||||||
|
'use strict'; // to work with ses lockdown
|
||||||
|
function wrap(obj, prop, target, receiver) {
|
||||||
|
const real = obj[prop];
|
||||||
|
obj[prop] = function(that) {
|
||||||
|
if (that === receiver) that = target;
|
||||||
|
const args = [].slice.call(arguments, 1);
|
||||||
|
return real.call(this, that, ...args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
wrap(Function.prototype, 'bind', target, receiver);
|
||||||
|
wrap(Function.prototype, 'apply', target, receiver);
|
||||||
|
} (global, proxy));
|
||||||
|
return proxy;
|
||||||
|
}
|
||||||
|
const proxy = setupProxy(globalThis);
|
||||||
|
with (proxy) {
|
||||||
|
with ({window: proxy, self: proxy, globalThis: proxy}) {
|
||||||
|
${content}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path of a file or folder inside the node_modules folder
|
* Get the path of a file or folder inside the node_modules folder
|
||||||
*
|
*
|
||||||
@ -147,4 +221,5 @@ module.exports = {
|
|||||||
isTestBuild,
|
isTestBuild,
|
||||||
logError,
|
logError,
|
||||||
getPathInsideNodeModules,
|
getPathInsideNodeModules,
|
||||||
|
wrapAgainstScuttling,
|
||||||
};
|
};
|
||||||
|
@ -52,12 +52,26 @@ function testIntrinsic(propertyName) {
|
|||||||
|
|
||||||
// As long as Object.isFrozen is the true Object.isFrozen, the object
|
// As long as Object.isFrozen is the true Object.isFrozen, the object
|
||||||
// it is called with cannot lie about being frozen.
|
// it is called with cannot lie about being frozen.
|
||||||
const value = globalThis[propertyName];
|
try {
|
||||||
if (value !== globalThis) {
|
const value = globalThis[propertyName];
|
||||||
assert.equal(
|
if (value !== globalThis) {
|
||||||
Object.isFrozen(value),
|
assert.equal(
|
||||||
true,
|
Object.isFrozen(value),
|
||||||
`value of universal property globalThis["${propertyName}"] should be frozen`,
|
true,
|
||||||
|
`value of universal property globalThis["${propertyName}"] should be frozen`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const lmre = // regex expression for LavaMoat scuttling error message
|
||||||
|
/LavaMoat - property "[A-Za-z0-9]*" of globalThis is inaccessible under scuttling mode/u;
|
||||||
|
|
||||||
|
if (!lmre.test(err.message)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.warn(
|
||||||
|
`Property ${propertyName} is not hardened`,
|
||||||
|
`because it is scuttled by LavaMoat protection.`,
|
||||||
|
`Visit https://github.com/LavaMoat/LavaMoat/pull/360 to learn more.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user