1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-29 07:16:36 +01:00
metamask-extension/development/sourcemap-validator.js
Mark Stacey 7199d9c567 Use externally hosted phishing warning page
An externally hosted phishing warning page is now used rather than the
built-in phishing warning page.The phishing page warning URL is set via
configuration file or environment variable. The default URL is either
the expected production URL or `http://localhost:9999/` for e2e testing
environments.

The new external phishing page includes a design change when it is
loaded within an iframe. In that case it now shows a condensed message,
and prompts the user to open the full warning page in a new tab to see
more details or bypass the warning. This is to prevent a clickjacking
attack from safelisting a site without user consent.

The new external phishing page also includes a simple caching service
worker to ensure it continues to work offline (or if our hosting goes
offline), as long as the user has successfully loaded the page at least
once. We also load the page temporarily during the extension startup
process to trigger the service worker installation.

The old phishing page and all related lines have been removed. The
property `web_accessible_resources` has also been removed from the
manifest. The only entry apart from the phishing page was `inpage.js`,
and we don't need that to be web accessible anymore because we inject
the script inline into each page rather than loading the file directly.

New e2e tests have been added to cover more phishing warning page
functionality, including the "safelist" action and the "iframe" case.
2022-05-16 14:40:50 -02:30

160 lines
4.4 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { SourceMapConsumer } = require('source-map');
const pify = require('pify');
const { codeFrameColumns } = require('@babel/code-frame');
const fsAsync = pify(fs);
//
// Utility to help check if sourcemaps are working
//
// searches `dist/chrome/inpage.js` for "new Error" statements
// and prints their source lines using the sourcemaps.
// if not working it may error or print minified garbage
//
start().catch((error) => {
console.error(error);
process.exit(1);
});
async function start() {
const targetFiles = [
`common-0.js`,
`background-0.js`,
`ui-0.js`,
// `contentscript.js`, skipped because the validator is erroneously sampling the inlined `inpage.js` script
`inpage.js`,
];
let valid = true;
for (const buildName of targetFiles) {
const fileIsValid = await validateSourcemapForFile({ buildName });
valid = valid && fileIsValid;
}
if (!valid) {
process.exit(1);
}
}
async function validateSourcemapForFile({ buildName }) {
console.log(`build "${buildName}"`);
const platform = `chrome`;
// load build and sourcemaps
let rawBuild;
try {
const filePath = path.join(
__dirname,
`/../dist/${platform}/`,
`${buildName}`,
);
rawBuild = await fsAsync.readFile(filePath, 'utf8');
} catch (_) {
// empty
}
if (!rawBuild) {
throw new Error(
`SourcemapValidator - failed to load source file for "${buildName}"`,
);
}
// attempt to load in dist mode
let rawSourceMap;
try {
const filePath = path.join(
__dirname,
`/../dist/sourcemaps/`,
`${buildName}.map`,
);
rawSourceMap = await fsAsync.readFile(filePath, 'utf8');
} catch (_) {
// empty
}
// attempt to load in dev mode
try {
const filePath = path.join(
__dirname,
`/../dist/${platform}/`,
`${buildName}.map`,
);
rawSourceMap = await fsAsync.readFile(filePath, 'utf8');
} catch (_) {
// empty
}
if (!rawSourceMap) {
throw new Error(
`SourcemapValidator - failed to load sourcemaps for "${buildName}"`,
);
}
const consumer = await new SourceMapConsumer(rawSourceMap);
const hasContentsOfAllSources = consumer.hasContentsOfAllSources();
if (!hasContentsOfAllSources) {
console.warn('SourcemapValidator - missing content of some sources...');
}
console.log(` sampling from ${consumer.sources.length} files`);
let sampleCount = 0;
let valid = true;
const buildLines = rawBuild.split('\n');
const targetString = 'new Error';
const matchesPerLine = buildLines.map((line) =>
indicesOf(targetString, line),
);
matchesPerLine.forEach((matchIndices, lineIndex) => {
matchIndices.forEach((matchColumn) => {
sampleCount += 1;
const position = { line: lineIndex + 1, column: matchColumn };
const result = consumer.originalPositionFor(position);
// warn if source content is missing
if (!result.source) {
valid = false;
const location = {
start: { line: position.line, column: position.column + 1 },
};
const codeSample = codeFrameColumns(rawBuild, location, {
message: `missing source for position`,
highlightCode: true,
});
console.error(
`missing source for position, in bundle "${buildName}"\n${codeSample}`,
);
return;
}
const sourceContent = consumer.sourceContentFor(result.source);
const sourceLines = sourceContent.split('\n');
const sourceLine = sourceLines[result.line - 1];
// this sometimes includes the whole line though we tried to match somewhere in the middle
const portion = sourceLine.slice(result.column);
const foundValidSource = portion.includes(targetString);
if (!foundValidSource) {
valid = false;
const location = {
start: { line: result.line + 1, column: result.column + 1 },
};
const codeSample = codeFrameColumns(sourceContent, location, {
message: `expected to see ${JSON.stringify(targetString)}`,
highlightCode: true,
});
console.error(
`Sourcemap seems invalid, ${result.source}\n${codeSample}`,
);
}
});
});
console.log(` checked ${sampleCount} samples`);
return valid;
}
function indicesOf(substring, string) {
const a = [];
let i = -1;
while ((i = string.indexOf(substring, i + 1)) >= 0) {
a.push(i);
}
return a;
}