1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +01:00

Activate LavaMoat scuttling security feature (#17276)

This commit is contained in:
weizman 2023-01-24 19:00:35 +02:00 committed by GitHub
parent f5426a84d9
commit 4a57d994c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 229 additions and 9 deletions

View File

@ -8,7 +8,7 @@ try {
* universalPropertyNames constant specified in 'ses/src/whitelist'. This
* function makes all function and object properties on the start compartment
* 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
* 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.
*/
(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);
// These named intrinsics are not automatically hardened by `lockdown`
@ -62,7 +65,18 @@ try {
}
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.`,
);
}
}
}
});

View File

@ -74,7 +74,52 @@ async function defineAndRunBuildTasks() {
} = await parseArgv();
// 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'];

View File

@ -38,6 +38,7 @@ const {
isTestBuild,
getEnvironment,
logError,
wrapAgainstScuttling,
} = require('./utils');
const {
@ -50,6 +51,42 @@ const {
createRemoveFencedCodeTransform,
} = 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.
*
@ -320,6 +357,7 @@ function createScriptTasks({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
});
}
@ -343,6 +381,7 @@ function createScriptTasks({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
});
}
@ -370,6 +409,7 @@ function createScriptTasks({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
}),
createNormalBundle({
buildTarget,
@ -382,6 +422,7 @@ function createScriptTasks({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
}),
);
}
@ -456,6 +497,7 @@ async function createManifestV3AppInitializationBundle({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
})();
// 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
* fences should be linted after fences have been removed.
* @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.
*/
function createNormalBundle({
@ -735,6 +778,7 @@ function createNormalBundle({
policyOnly,
shouldLintFenceFiles,
version,
applyLavaMoat,
}) {
return async function () {
// create bundler setup and apply defaults
@ -763,6 +807,7 @@ function createNormalBundle({
minify,
reloadOnChange,
shouldLintFenceFiles,
applyLavaMoat,
});
// set bundle entries
@ -812,6 +857,7 @@ function setupBundlerDefaults(
minify,
reloadOnChange,
shouldLintFenceFiles,
applyLavaMoat,
},
) {
const { bundlerOpts } = buildConfiguration;
@ -878,6 +924,9 @@ function setupBundlerDefaults(
// Setup source maps
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 }) {
const { events } = buildConfiguration;
events.on('configurePipeline', ({ pipeline }) => {
@ -976,6 +1046,8 @@ async function createBundle(buildConfiguration, { reloadOnChange }) {
[],
'vinyl',
[],
'scuttle',
[],
'sourcemaps:init',
[],
'minify',

View File

@ -118,6 +118,80 @@ function logError(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
*
@ -147,4 +221,5 @@ module.exports = {
isTestBuild,
logError,
getPathInsideNodeModules,
wrapAgainstScuttling,
};

View File

@ -52,12 +52,26 @@ function testIntrinsic(propertyName) {
// As long as Object.isFrozen is the true Object.isFrozen, the object
// it is called with cannot lie about being frozen.
const value = globalThis[propertyName];
if (value !== globalThis) {
assert.equal(
Object.isFrozen(value),
true,
`value of universal property globalThis["${propertyName}"] should be frozen`,
try {
const value = globalThis[propertyName];
if (value !== globalThis) {
assert.equal(
Object.isFrozen(value),
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.`,
);
}