Merge pull request #14840 from MetaMask/master-sync

Sync `master` with `develop`
feature/default_network_editable
Mark Stacey 3 years ago committed by GitHub
commit 43bd885be7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .depcheckrc.yml
  2. 3
      .metamaskrc.dist
  3. 42
      CHANGELOG.md
  4. 3
      app/manifest/v2/_base.json
  5. 150
      app/phishing.html
  6. 86
      app/scripts/background.js
  7. 61
      app/scripts/contentscript.js
  8. 2
      app/scripts/lib/util.js
  9. 7
      app/scripts/lib/util.test.js
  10. 31
      app/scripts/metamask-controller.js
  11. 40
      app/scripts/phishing-detect.js
  12. 67
      development/build/scripts.js
  13. 4
      development/build/static.js
  14. 1
      development/sourcemap-validator.js
  15. 1427
      lavamoat/build-system/policy.json
  16. 3
      package.json
  17. 11
      test/e2e/helpers.js
  18. 20
      test/e2e/mock-page-with-disallowed-iframe/index.html
  19. 11
      test/e2e/mock-page-with-iframe/index.html
  20. 58
      test/e2e/phishing-warning-page-server.js
  21. 89
      test/e2e/tests/phishing-detection.spec.js
  22. 4
      test/e2e/webdriver/driver.js
  23. 18
      yarn.lock

@ -17,6 +17,7 @@ ignores:
# used in testing + ci # used in testing + ci
- '@metamask/auto-changelog' # invoked as `auto-changelog` - '@metamask/auto-changelog' # invoked as `auto-changelog`
- '@metamask/forwarder' - '@metamask/forwarder'
- '@metamask/phishing-warning' # statically hosted as part of some e2e tests
- '@metamask/test-dapp' - '@metamask/test-dapp'
- '@metamask/design-tokens' # Only imported in index.css - '@metamask/design-tokens' # Only imported in index.css
- '@tsconfig/node14' # required dynamically by TS, used in tsconfig.json - '@tsconfig/node14' # required dynamically by TS, used in tsconfig.json

@ -6,3 +6,6 @@ ONBOARDING_V2=
SWAPS_USE_DEV_APIS= SWAPS_USE_DEV_APIS=
COLLECTIBLES_V1= COLLECTIBLES_V1=
TOKEN_DETECTION_V2= TOKEN_DETECTION_V2=
; Set this to test changes to the phishing warning page.
PHISHING_WARNING_PAGE_URL=

@ -6,6 +6,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [10.14.7]
### Changed
- Make JavaScript bundles more reproducible between environments.
- The bundles no longer include absolute paths to each module included.
## [10.14.6]
### Changed
- Move phishing warning page to external site.
- The page shown when a site is blocked has been extracted from the extension and moved to an external site. This site is eagerly cached with a service worker upon extension startup, so it should continue to work even while offline.
- Make build .zip files reproducible (#14623)
- The ordering of files within each .zip file was non-deterministic before this change. We fixed this to comply with Firefox store policies.
## [10.14.5]
### Fixed
- This release was deployed to fix a configuration issue.
## [10.14.4]
### Fixed
- This release was deployed to fix a configuration issue.
## [10.14.3]
### Fixed
- This release was deployed to fix a configuration issue.
## [10.14.2]
### Fixed
- Make build deterministic (#14610)
- The ordering of modules within each bundle was non-deterministic before this change. We fixed this to comply with Firefox store policies.
## [10.14.1]
### Changed
- This version was used to rollback from v10.14.0 to v10.13.0.
## [10.14.0] ## [10.14.0]
### Added ### Added
- **[FLASK]** Add snap version to details page ([#14110](https://github.com/MetaMask/metamask-extension/pull/14110)) - **[FLASK]** Add snap version to details page ([#14110](https://github.com/MetaMask/metamask-extension/pull/14110))
@ -2908,7 +2941,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Uncategorized ### Uncategorized
- Added the ability to restore accounts from seed words. - Added the ability to restore accounts from seed words.
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.14.0...HEAD [Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.14.7...HEAD
[10.14.7]: https://github.com/MetaMask/metamask-extension/compare/v10.14.6...v10.14.7
[10.14.6]: https://github.com/MetaMask/metamask-extension/compare/v10.14.5...v10.14.6
[10.14.5]: https://github.com/MetaMask/metamask-extension/compare/v10.14.4...v10.14.5
[10.14.4]: https://github.com/MetaMask/metamask-extension/compare/v10.14.3...v10.14.4
[10.14.3]: https://github.com/MetaMask/metamask-extension/compare/v10.14.2...v10.14.3
[10.14.2]: https://github.com/MetaMask/metamask-extension/compare/v10.14.1...v10.14.2
[10.14.1]: https://github.com/MetaMask/metamask-extension/compare/v10.14.0...v10.14.1
[10.14.0]: https://github.com/MetaMask/metamask-extension/compare/v10.13.0...v10.14.0 [10.14.0]: https://github.com/MetaMask/metamask-extension/compare/v10.13.0...v10.14.0
[10.13.0]: https://github.com/MetaMask/metamask-extension/compare/v10.12.4...v10.13.0 [10.13.0]: https://github.com/MetaMask/metamask-extension/compare/v10.12.4...v10.13.0
[10.12.4]: https://github.com/MetaMask/metamask-extension/compare/v10.12.3...v10.12.4 [10.12.4]: https://github.com/MetaMask/metamask-extension/compare/v10.12.3...v10.12.4

@ -72,6 +72,5 @@
"*://*.eth/", "*://*.eth/",
"notifications" "notifications"
], ],
"short_name": "__MSG_appName__", "short_name": "__MSG_appName__"
"web_accessible_resources": ["inpage.js", "phishing.html"]
} }

@ -1,150 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>MetaMask Phishing Detection</title>
<script
src="./globalthis.js"
type="text/javascript"
charset="utf-8"
></script>
<script
src="./lockdown-install.js"
type="text/javascript"
charset="utf-8"
></script>
<script
src="./lockdown-run.js"
type="text/javascript"
charset="utf-8"
></script>
<script
src="./lockdown-more.js"
type="text/javascript"
charset="utf-8"
></script>
<script src="./phishing-detect.js"></script>
<link rel="stylesheet" type="text/css" href="./index.css" title="ltr" />
<link
rel="stylesheet"
type="text/css"
href="./index-rtl.css"
title="rtl"
disabled
/>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body,
html {
background-color: var(--color-error-default);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: Roboto, Arial, sans-serif;
width: 100vw;
min-height: 100vh;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
width: 80%;
background-color: var(--color-background-default);
box-shadow: 0 0 15px #737373;
}
.content__header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
color: var(--color-error-default);
border-bottom: 1px solid var(--color-border-default);
padding: 2em;
}
.content__header h1 {
font-size: 24px;
font-weight: normal;
}
.content__header h1 i {
margin-right: 0.25em;
}
.content__header img {
margin-bottom: 3em;
width: 130px;
}
.content__body {
background-color: var(--color-background-alternative);
font-size: 12pt;
color: var(--color-text-default);
}
.content__body p {
margin: 2em;
}
.content__body p a {
text-decoration: underline;
color: var(--color-primary-default);
cursor: pointer;
}
</style>
</head>
<body>
<div class="content">
<div class="content__header">
<img src="./images/logo/metamask-fox.svg" alt="MetaMask Logo" />
<h1>
<i class="fa fa-exclamation-circle" aria-hidden="true"></i>
MetaMask Phishing Detection
</h1>
</div>
<div class="content__body">
<p>
This domain is currently on the MetaMask domain warning list. This
means that based on information available to us, MetaMask believes
this domain could currently compromise your security and, as an added
safety feature, MetaMask has restricted access to the site. To
override this, please read the rest of this warning for instructions
on how to continue at your own risk.
</p>
<p>
There are many reasons sites can appear on our warning list, and our
warning list compiles from other widely used industry lists. Such
reasons can include known fraud or security risks, such as domains
that test positive on the
<a href="https://github.com/metamask/eth-phishing-detect"
>Ethereum Phishing Detector</a
>. Domains on these warning lists may include outright malicious
websites and legitimate websites that have been compromised by a
malicious actor.
</p>
<p>
To read more about this site
<a id="csdbLink" href="https://cryptoscamdb.org/search"
>please search for the domain on CryptoScamDB</a
>.
</p>
<p>
Note that this warning list is compiled on a voluntary basis. This
list may be inaccurate or incomplete. Just because a domain does not
appear on this list is not an implicit guarantee of that domain's
safety. As always, your transactions are your own responsibility. If
you wish to interact with any domain on our warning list, you can do
so by <a id="unsafe-continue">continuing at your own risk</a>.
</p>
<p>
If you think this domain is incorrectly flagged or if a blocked
legitimate website has resolved its security issues,
<a
id="new-issue-link"
href="https://github.com/metamask/eth-phishing-detect/issues/new"
>please file an issue</a
>.
</p>
</div>
</div>
</body>
</html>

@ -76,6 +76,12 @@ if (inTest || process.env.METAMASK_DEBUG) {
global.metamaskGetState = localStore.get.bind(localStore); global.metamaskGetState = localStore.get.bind(localStore);
} }
const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
const ONE_SECOND_IN_MILLISECONDS = 1_000;
// Timeout for initializing phishing warning page.
const PHISHING_WARNING_PAGE_TIMEOUT = ONE_SECOND_IN_MILLISECONDS;
/** /**
* In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised. * In case of MV3 we attach a "onConnect" event listener as soon as the application is initialised.
* Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated. * Reason is that in case of MV3 a delay in doing this was resulting in missing first connect event after service worker is re-activated.
@ -159,9 +165,76 @@ async function initialize(remotePort) {
const initState = await loadStateFromPersistence(); const initState = await loadStateFromPersistence();
const initLangCode = await getFirstPreferredLangCode(); const initLangCode = await getFirstPreferredLangCode();
await setupController(initState, initLangCode, remotePort); await setupController(initState, initLangCode, remotePort);
await loadPhishingWarningPage();
log.info('MetaMask initialization complete.'); log.info('MetaMask initialization complete.');
} }
/**
* An error thrown if the phishing warning page takes too long to load.
*/
class PhishingWarningPageTimeoutError extends Error {
constructor() {
super('Timeout failed');
}
}
/**
* Load the phishing warning page temporarily to ensure the service
* worker has been registered, so that the warning page works offline.
*/
async function loadPhishingWarningPage() {
let iframe;
try {
const extensionStartupPhishingPageUrl = new URL(
process.env.PHISHING_WARNING_PAGE_URL,
);
// The `extensionStartup` hash signals to the phishing warning page that it should not bother
// setting up streams for user interaction. Otherwise this page load would cause a console
// error.
extensionStartupPhishingPageUrl.hash = '#extensionStartup';
iframe = window.document.createElement('iframe');
iframe.setAttribute('src', extensionStartupPhishingPageUrl.href);
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin');
// Create "deferred Promise" to allow passing resolve/reject to event handlers
let deferredResolve;
let deferredReject;
const loadComplete = new Promise((resolve, reject) => {
deferredResolve = resolve;
deferredReject = reject;
});
// The load event is emitted once loading has completed, even if the loading failed.
// If loading failed we can't do anything about it, so we don't need to check.
iframe.addEventListener('load', deferredResolve);
// This step initiates the page loading.
window.document.body.appendChild(iframe);
// This timeout ensures that this iframe gets cleaned up in a reasonable
// timeframe, and ensures that the "initialization complete" message
// doesn't get delayed too long.
setTimeout(
() => deferredReject(new PhishingWarningPageTimeoutError()),
PHISHING_WARNING_PAGE_TIMEOUT,
);
await loadComplete;
} catch (error) {
if (error instanceof PhishingWarningPageTimeoutError) {
console.warn(
'Phishing warning page timeout; page not guaraneteed to work offline.',
);
} else {
console.error('Failed to initialize phishing warning page', error);
}
} finally {
if (iframe) {
iframe.remove();
}
}
}
// //
// State and Persistence // State and Persistence
// //
@ -384,6 +457,10 @@ function setupController(initState, initLangCode, remoteSourcePort) {
remotePort.sender.origin === `chrome-extension://${browser.runtime.id}`; remotePort.sender.origin === `chrome-extension://${browser.runtime.id}`;
} }
const senderUrl = remotePort.sender?.url
? new URL(remotePort.sender.url)
: null;
if (isMetaMaskInternalProcess) { if (isMetaMaskInternalProcess) {
const portStream = new PortStream(remotePort); const portStream = new PortStream(remotePort);
// communication with popup // communication with popup
@ -435,6 +512,15 @@ function setupController(initState, initLangCode, remoteSourcePort) {
); );
}); });
} }
} else if (
senderUrl &&
senderUrl.origin === phishingPageUrl.origin &&
senderUrl.pathname === phishingPageUrl.pathname
) {
const portStream = new PortStream(remotePort);
controller.setupPhishingCommunication({
connectionStream: portStream,
});
} else { } else {
if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) { if (remotePort.sender && remotePort.sender.tab && remotePort.sender.url) {
const tabId = remotePort.sender.tab.id; const tabId = remotePort.sender.tab.id;

@ -19,8 +19,13 @@ const inpageContent = fs.readFileSync(
const inpageSuffix = `//# sourceURL=${browser.runtime.getURL('inpage.js')}\n`; const inpageSuffix = `//# sourceURL=${browser.runtime.getURL('inpage.js')}\n`;
const inpageBundle = inpageContent + inpageSuffix; const inpageBundle = inpageContent + inpageSuffix;
// contexts
const CONTENT_SCRIPT = 'metamask-contentscript'; const CONTENT_SCRIPT = 'metamask-contentscript';
const INPAGE = 'metamask-inpage'; const INPAGE = 'metamask-inpage';
const PHISHING_WARNING_PAGE = 'metamask-phishing-warning-page';
// stream channels
const PHISHING_SAFELIST = 'metamask-phishing-safelist';
const PROVIDER = 'metamask-provider'; const PROVIDER = 'metamask-provider';
// TODO:LegacyProvider: Delete // TODO:LegacyProvider: Delete
@ -29,7 +34,14 @@ const LEGACY_INPAGE = 'inpage';
const LEGACY_PROVIDER = 'provider'; const LEGACY_PROVIDER = 'provider';
const LEGACY_PUBLIC_CONFIG = 'publicConfig'; const LEGACY_PUBLIC_CONFIG = 'publicConfig';
if (shouldInjectProvider()) { const phishingPageUrl = new URL(process.env.PHISHING_WARNING_PAGE_URL);
if (
window.location.origin === phishingPageUrl.origin &&
window.location.pathname === phishingPageUrl.pathname
) {
setupPhishingStream();
} else if (shouldInjectProvider()) {
injectScript(inpageBundle); injectScript(inpageBundle);
setupStreams(); setupStreams();
} }
@ -57,6 +69,47 @@ function injectScript(content) {
} }
} }
async function setupPhishingStream() {
// the transport-specific streams for communication between inpage and background
const pageStream = new WindowPostMessageStream({
name: CONTENT_SCRIPT,
target: PHISHING_WARNING_PAGE,
});
const extensionPort = browser.runtime.connect({ name: CONTENT_SCRIPT });
const extensionStream = new PortStream(extensionPort);
// create and connect channel muxers
// so we can handle the channels individually
const pageMux = new ObjectMultiplex();
pageMux.setMaxListeners(25);
const extensionMux = new ObjectMultiplex();
extensionMux.setMaxListeners(25);
pump(pageMux, pageStream, pageMux, (err) =>
logStreamDisconnectWarning('MetaMask Inpage Multiplex', err),
);
pump(extensionMux, extensionStream, extensionMux, (err) => {
logStreamDisconnectWarning('MetaMask Background Multiplex', err);
window.postMessage(
{
target: PHISHING_WARNING_PAGE, // the post-message-stream "target"
data: {
// this object gets passed to obj-multiplex
name: PHISHING_SAFELIST, // the obj-multiplex channel name
data: {
jsonrpc: '2.0',
method: 'METAMASK_STREAM_FAILURE',
},
},
},
window.location.origin,
);
});
// forward communication across inpage-background for these channels only
forwardTrafficBetweenMuxes(PHISHING_SAFELIST, pageMux, extensionMux);
}
/** /**
* Sets up two-way communication streams between the * Sets up two-way communication streams between the
* browser extension and local per-page browser context. * browser extension and local per-page browser context.
@ -307,9 +360,9 @@ function blockedDomainCheck() {
* Redirects the current page to a phishing information page * Redirects the current page to a phishing information page
*/ */
function redirectToPhishingWarning() { function redirectToPhishingWarning() {
console.debug('MetaMask: Routing to Phishing Warning component.'); console.debug('MetaMask: Routing to Phishing Warning page.');
const extensionURL = browser.runtime.getURL('phishing.html'); const baseUrl = process.env.PHISHING_WARNING_PAGE_URL;
window.location.href = `${extensionURL}#${querystring.stringify({ window.location.href = `${baseUrl}#${querystring.stringify({
hostname: window.location.hostname, hostname: window.location.hostname,
href: window.location.href, href: window.location.href,
})}`; })}`;

@ -27,7 +27,7 @@ const getEnvironmentTypeMemo = memoize((url) => {
const parsedUrl = new URL(url); const parsedUrl = new URL(url);
if (parsedUrl.pathname === '/popup.html') { if (parsedUrl.pathname === '/popup.html') {
return ENVIRONMENT_TYPE_POPUP; return ENVIRONMENT_TYPE_POPUP;
} else if (['/home.html', '/phishing.html'].includes(parsedUrl.pathname)) { } else if (['/home.html'].includes(parsedUrl.pathname)) {
return ENVIRONMENT_TYPE_FULLSCREEN; return ENVIRONMENT_TYPE_FULLSCREEN;
} else if (parsedUrl.pathname === '/notification.html') { } else if (parsedUrl.pathname === '/notification.html') {
return ENVIRONMENT_TYPE_NOTIFICATION; return ENVIRONMENT_TYPE_NOTIFICATION;

@ -34,13 +34,6 @@ describe('app utils', () => {
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN); expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN);
}); });
it('should return fullscreen type for phishing.html', () => {
const environmentType = getEnvironmentType(
'http://extension-id/phishing.html',
);
expect(environmentType).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN);
});
it('should return background type', () => { it('should return background type', () => {
const environmentType = getEnvironmentType( const environmentType = getEnvironmentType(
'http://extension-id/_generated_background_page.html', 'http://extension-id/_generated_background_page.html',

@ -155,6 +155,9 @@ export const METAMASK_CONTROLLER_EVENTS = {
APPROVAL_STATE_CHANGE: 'ApprovalController:stateChange', APPROVAL_STATE_CHANGE: 'ApprovalController:stateChange',
}; };
// stream channels
const PHISHING_SAFELIST = 'metamask-phishing-safelist';
export default class MetamaskController extends EventEmitter { export default class MetamaskController extends EventEmitter {
/** /**
* @param {Object} opts * @param {Object} opts
@ -1503,7 +1506,6 @@ export default class MetamaskController extends EventEmitter {
), ),
markPasswordForgotten: this.markPasswordForgotten.bind(this), markPasswordForgotten: this.markPasswordForgotten.bind(this),
unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this), unMarkPasswordForgotten: this.unMarkPasswordForgotten.bind(this),
safelistPhishingDomain: this.safelistPhishingDomain.bind(this),
getRequestAccountTabIds: this.getRequestAccountTabIds, getRequestAccountTabIds: this.getRequestAccountTabIds,
getOpenMetamaskTabsIds: this.getOpenMetamaskTabsIds, getOpenMetamaskTabsIds: this.getOpenMetamaskTabsIds,
markNotificationPopupAsAutomaticallyClosed: () => markNotificationPopupAsAutomaticallyClosed: () =>
@ -3309,6 +3311,33 @@ export default class MetamaskController extends EventEmitter {
); );
} }
/**
* Used to create a multiplexed stream for connecting to the phishing warning page.
*
* @param options - Options bag.
* @param {ReadableStream} options.connectionStream - The Duplex stream to connect to.
*/
setupPhishingCommunication({ connectionStream }) {
const { usePhishDetect } = this.preferencesController.store.getState();
if (!usePhishDetect) {
return;
}
// setup multiplexing
const mux = setupMultiplex(connectionStream);
const phishingStream = mux.createStream(PHISHING_SAFELIST);
// set up postStream transport
phishingStream.on(
'data',
createMetaRPCHandler(
{ safelistPhishingDomain: this.safelistPhishingDomain.bind(this) },
phishingStream,
),
);
}
/** /**
* Called when we detect a suspicious domain. Requests the browser redirects * Called when we detect a suspicious domain. Requests the browser redirects
* to our anti-phishing page. * to our anti-phishing page.

@ -1,40 +0,0 @@
import querystring from 'querystring';
import PortStream from 'extension-port-stream';
import browser from 'webextension-polyfill';
import createRandomId from '../../shared/modules/random-id';
import { setupMultiplex } from './lib/stream-utils';
import { getEnvironmentType } from './lib/util';
import ExtensionPlatform from './platforms/extension';
document.addEventListener('DOMContentLoaded', start);
function start() {
const hash = window.location.hash.substring(1);
const suspect = querystring.parse(hash);
const newIssueLink = document.getElementById('new-issue-link');
const newIssueUrl = `https://github.com/MetaMask/eth-phishing-detect/issues/new`;
const newIssueParams = `?title=[Legitimate%20Site%20Blocked]%20${encodeURIComponent(
suspect.hostname,
)}&body=${encodeURIComponent(suspect.href)}`;
newIssueLink.href = `${newIssueUrl}${newIssueParams}`;
global.platform = new ExtensionPlatform();
const extensionPort = browser.runtime.connect({
name: getEnvironmentType(),
});
const connectionStream = new PortStream(extensionPort);
const mx = setupMultiplex(connectionStream);
const backgroundConnection = mx.createStream('controller');
const continueLink = document.getElementById('unsafe-continue');
continueLink.addEventListener('click', () => {
backgroundConnection.write({
jsonrpc: '2.0',
method: 'safelistPhishingDomain',
params: [suspect.hostname],
id: createRandomId(),
});
window.location.href = suspect.href;
});
}

@ -34,6 +34,7 @@ const metamaskrc = require('rc')('metamask', {
INFURA_PROD_PROJECT_ID: process.env.INFURA_PROD_PROJECT_ID, INFURA_PROD_PROJECT_ID: process.env.INFURA_PROD_PROJECT_ID,
ONBOARDING_V2: process.env.ONBOARDING_V2, ONBOARDING_V2: process.env.ONBOARDING_V2,
COLLECTIBLES_V1: process.env.COLLECTIBLES_V1, COLLECTIBLES_V1: process.env.COLLECTIBLES_V1,
PHISHING_WARNING_PAGE_URL: process.env.PHISHING_WARNING_PAGE_URL,
TOKEN_DETECTION_V2: process.env.TOKEN_DETECTION_V2, TOKEN_DETECTION_V2: process.env.TOKEN_DETECTION_V2,
SEGMENT_HOST: process.env.SEGMENT_HOST, SEGMENT_HOST: process.env.SEGMENT_HOST,
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY, SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY,
@ -133,6 +134,48 @@ function getSegmentWriteKey({ buildType, environment }) {
throw new Error(`Invalid build type: '${buildType}'`); throw new Error(`Invalid build type: '${buildType}'`);
} }
/**
* Get the URL for the phishing warning page, if it has been set.
*
* @param {object} options - The phishing warning page options.
* @param {boolean} options.testing - Whether this is a test build or not.
* @returns {string} The URL for the phishing warning page, or `undefined` if no URL is set.
*/
function getPhishingWarningPageUrl({ testing }) {
let phishingWarningPageUrl = metamaskrc.PHISHING_WARNING_PAGE_URL;
if (!phishingWarningPageUrl) {
phishingWarningPageUrl = testing
? 'http://localhost:9999/'
: 'https://metamask.github.io/phishing-warning/v1.1.0/';
}
// We add a hash/fragment to the URL dynamically, so we need to ensure it
// has a valid pathname to append a hash to.
const normalizedUrl = phishingWarningPageUrl.endsWith('/')
? phishingWarningPageUrl
: `${phishingWarningPageUrl}/`;
let phishingWarningPageUrlObject;
try {
// eslint-disable-next-line no-new
phishingWarningPageUrlObject = new URL(normalizedUrl);
} catch (error) {
throw new Error(
`Invalid phishing warning page URL: '${normalizedUrl}'`,
error,
);
}
if (phishingWarningPageUrlObject.hash) {
// The URL fragment must be set dynamically
throw new Error(
`URL fragment not allowed in phishing warning page URL: '${normalizedUrl}'`,
);
}
return normalizedUrl;
}
const noopWriteStream = through.obj((_file, _fileEncoding, callback) => const noopWriteStream = through.obj((_file, _fileEncoding, callback) =>
callback(), callback(),
); );
@ -218,11 +261,6 @@ function createScriptTasks({
createTaskForBundleSentry({ devMode, testing }), createTaskForBundleSentry({ devMode, testing }),
); );
const phishingDetectSubtask = createTask(
`${taskPrefix}:phishing-detect`,
createTaskForBundlePhishingDetect({ devMode, testing }),
);
// task for initiating browser livereload // task for initiating browser livereload
const initiateLiveReload = async () => { const initiateLiveReload = async () => {
if (devMode) { if (devMode) {
@ -245,7 +283,6 @@ function createScriptTasks({
contentscriptSubtask, contentscriptSubtask,
disableConsoleSubtask, disableConsoleSubtask,
installSentrySubtask, installSentrySubtask,
phishingDetectSubtask,
].map((subtask) => ].map((subtask) =>
runInChildProcess(subtask, { runInChildProcess(subtask, {
applyLavaMoat, applyLavaMoat,
@ -293,23 +330,6 @@ function createScriptTasks({
}); });
} }
function createTaskForBundlePhishingDetect({ devMode, testing }) {
const label = 'phishing-detect';
return createNormalBundle({
buildType,
browserPlatforms,
destFilepath: `${label}.js`,
devMode,
entryFilepath: `./app/scripts/${label}.js`,
ignoredFiles,
label,
testing,
policyOnly,
shouldLintFenceFiles,
version,
});
}
// the "contentscript" bundle contains the "inpage" bundle // the "contentscript" bundle contains the "inpage" bundle
function createTaskForBundleContentscript({ devMode, testing }) { function createTaskForBundleContentscript({ devMode, testing }) {
const inpage = 'inpage'; const inpage = 'inpage';
@ -882,6 +902,7 @@ function getEnvironmentVariables({ buildType, devMode, testing, version }) {
METAMASK_BUILD_TYPE: buildType, METAMASK_BUILD_TYPE: buildType,
NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION, NODE_ENV: devMode ? ENVIRONMENT.DEVELOPMENT : ENVIRONMENT.PRODUCTION,
IN_TEST: testing, IN_TEST: testing,
PHISHING_WARNING_PAGE_URL: getPhishingWarningPageUrl({ testing }),
PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '', PUBNUB_SUB_KEY: process.env.PUBNUB_SUB_KEY || '',
PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '', PUBNUB_PUB_KEY: process.env.PUBNUB_PUB_KEY || '',
CONF: devMode ? metamaskrc : {}, CONF: devMode ? metamaskrc : {},

@ -174,10 +174,6 @@ function getCopyTargets(shouldIncludeLockdown) {
src: require.resolve('@lavamoat/lavapack/src/runtime.js'), src: require.resolve('@lavamoat/lavapack/src/runtime.js'),
dest: `runtime-lavamoat.js`, dest: `runtime-lavamoat.js`,
}, },
{
src: `./app/phishing.html`,
dest: `phishing.html`,
},
]; ];
const languageTags = new Set(); const languageTags = new Set();

@ -24,7 +24,6 @@ async function start() {
`common-0.js`, `common-0.js`,
`background-0.js`, `background-0.js`,
`ui-0.js`, `ui-0.js`,
'phishing-detect.js',
// `contentscript.js`, skipped because the validator is erroneously sampling the inlined `inpage.js` script // `contentscript.js`, skipped because the validator is erroneously sampling the inlined `inpage.js` script
`inpage.js`, `inpage.js`,
]; ];

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "metamask-crx", "name": "metamask-crx",
"version": "10.14.0", "version": "10.14.7",
"private": true, "private": true,
"repository": { "repository": {
"type": "git", "type": "git",
@ -251,6 +251,7 @@
"@metamask/eslint-config-nodejs": "^9.0.0", "@metamask/eslint-config-nodejs": "^9.0.0",
"@metamask/eslint-config-typescript": "^9.0.1", "@metamask/eslint-config-typescript": "^9.0.1",
"@metamask/forwarder": "^1.1.0", "@metamask/forwarder": "^1.1.0",
"@metamask/phishing-warning": "^1.1.0",
"@metamask/test-dapp": "^5.0.0", "@metamask/test-dapp": "^5.0.0",
"@sentry/cli": "^1.58.0", "@sentry/cli": "^1.58.0",
"@storybook/addon-a11y": "^6.3.12", "@storybook/addon-a11y": "^6.3.12",

@ -6,6 +6,7 @@ const enLocaleMessages = require('../../app/_locales/en/messages.json');
const { setupMocking } = require('./mock-e2e'); const { setupMocking } = require('./mock-e2e');
const Ganache = require('./ganache'); const Ganache = require('./ganache');
const FixtureServer = require('./fixture-server'); const FixtureServer = require('./fixture-server');
const PhishingWarningPageServer = require('./phishing-warning-page-server');
const { buildWebDriver } = require('./webdriver'); const { buildWebDriver } = require('./webdriver');
const { ensureXServerIsRunning } = require('./x-server'); const { ensureXServerIsRunning } = require('./x-server');
@ -27,6 +28,7 @@ async function withFixtures(options, testSuite) {
title, title,
failOnConsoleError = true, failOnConsoleError = true,
dappPath = undefined, dappPath = undefined,
dappPaths,
testSpecificMock = function () { testSpecificMock = function () {
// do nothing. // do nothing.
}, },
@ -38,6 +40,7 @@ async function withFixtures(options, testSuite) {
let secondaryGanacheServer; let secondaryGanacheServer;
let numberOfDapps = dapp ? 1 : 0; let numberOfDapps = dapp ? 1 : 0;
const dappServer = []; const dappServer = [];
const phishingPageServer = new PhishingWarningPageServer();
let webDriver; let webDriver;
let failed = false; let failed = false;
@ -55,14 +58,15 @@ async function withFixtures(options, testSuite) {
} }
await fixtureServer.start(); await fixtureServer.start();
await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures)); await fixtureServer.loadState(path.join(__dirname, 'fixtures', fixtures));
await phishingPageServer.start();
if (dapp) { if (dapp) {
if (dappOptions?.numberOfDapps) { if (dappOptions?.numberOfDapps) {
numberOfDapps = dappOptions.numberOfDapps; numberOfDapps = dappOptions.numberOfDapps;
} }
for (let i = 0; i < numberOfDapps; i++) { for (let i = 0; i < numberOfDapps; i++) {
let dappDirectory; let dappDirectory;
if (dappPath) { if (dappPath || (dappPaths && dappPaths[i])) {
dappDirectory = path.resolve(__dirname, dappPath); dappDirectory = path.resolve(__dirname, dappPath || dappPaths[i]);
} else { } else {
dappDirectory = path.resolve( dappDirectory = path.resolve(
__dirname, __dirname,
@ -146,6 +150,9 @@ async function withFixtures(options, testSuite) {
} }
} }
} }
if (phishingPageServer.isRunning()) {
await phishingPageServer.quit();
}
await mockServer.stop(); await mockServer.stop();
} }
} }

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<title>Mock E2E Phishing Page</title>
</head>
<script type="text/javascript">
function setIframeSource() {
const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
const extensionUrl = new URL(params.extensionUrl);
document.getElementById('frame').src = `http://localhost:9999/#hostname=${encodeURIComponent(extensionUrl.hostname)}&href=${encodeURIComponent(extensionUrl.href)}`;
}
window.onload = setIframeSource;
</script>
<body>
<div>Hello</div>
<iframe id="frame" width=900 height=900>
</body>
</html>

@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<title>Mock E2E Phishing Page</title>
</head>
<body>
<div>Hello</div>
<iframe src="http://127.0.0.1:8081" width=900 height=900>
</body>
</html>

@ -0,0 +1,58 @@
const path = require('path');
const createStaticServer = require('../../development/create-static-server');
const phishingWarningDirectory = path.resolve(
__dirname,
'..',
'..',
'node_modules',
'@metamask',
'phishing-warning',
'dist',
);
class PhishingWarningPageServer {
constructor() {
this._server = createStaticServer(phishingWarningDirectory);
}
async start({ port = 9999 } = {}) {
this._server.listen(port);
let resolveStart;
let rejectStart;
const result = new Promise((resolve, reject) => {
resolveStart = resolve;
rejectStart = reject;
});
this._server.once('listening', resolveStart);
this._server.once('error', rejectStart);
try {
await result;
// clean up listener to ensure later errors properly bubble up
this._server.removeListener('error', rejectStart);
} catch (error) {
this._server.removeListener('listening', resolveStart);
throw error;
}
}
isRunning() {
return this._server.listening;
}
async quit() {
await new Promise((resolve, reject) =>
this._server.close((error) => {
if (error) {
reject(error);
} else {
resolve();
}
}),
);
}
}
module.exports = PhishingWarningPageServer;

@ -15,7 +15,7 @@ describe('Phishing Detection', function () {
tolerance: 2, tolerance: 2,
fuzzylist: [], fuzzylist: [],
whitelist: [], whitelist: [],
blacklist: ['example.com'], blacklist: ['127.0.0.1'],
}, },
}; };
}); });
@ -29,22 +29,101 @@ describe('Phishing Detection', function () {
}, },
], ],
}; };
it('should display the MetaMask Phishing Detection page', async function () { it('should display the MetaMask Phishing Detection page and take the user to the blocked page if they continue', async function () {
await withFixtures( await withFixtures(
{ {
fixtures: 'imported-account', fixtures: 'imported-account',
ganacheOptions, ganacheOptions,
title: this.test.title, title: this.test.title,
testSpecificMock: mockPhishingDetection, testSpecificMock: mockPhishingDetection,
dapp: true,
failOnConsoleError: false,
}, },
async ({ driver }) => { async ({ driver }) => {
await driver.navigate(); await driver.navigate();
await driver.fill('#password', 'correct horse battery staple'); await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER); await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://example.com'); await driver.openNewPage('http://127.0.0.1:8080');
await driver.waitForSelector({ text: 'continuing at your own risk' }); await driver.clickElement({
text: 'continuing at your own risk',
});
const header = await driver.findElement('h1');
assert.equal(await header.getText(), 'E2E Test Dapp');
},
);
});
it('should display the MetaMask Phishing Detection page in an iframe and take the user to the blocked page if they continue', async function () {
await withFixtures(
{
fixtures: 'imported-account',
ganacheOptions,
title: this.test.title,
testSpecificMock: mockPhishingDetection,
dapp: true,
dappPaths: ['mock-page-with-iframe'],
dappOptions: {
numberOfDapps: 2,
},
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage('http://localhost:8080/');
const iframe = await driver.findElement('iframe');
await driver.switchToFrame(iframe);
await driver.clickElement({
text: 'Open this warning in a new tab',
});
await driver.switchToWindowWithTitle('MetaMask Phishing Detection');
await driver.clickElement({
text: 'continuing at your own risk',
});
const header = await driver.findElement('h1'); const header = await driver.findElement('h1');
assert.equal(await header.getText(), 'MetaMask Phishing Detection'); assert.equal(await header.getText(), 'E2E Test Dapp');
},
);
});
it('should display the MetaMask Phishing Detection page in an iframe but should NOT take the user to the blocked page if it is not an accessible resource', async function () {
await withFixtures(
{
fixtures: 'imported-account',
ganacheOptions,
title: this.test.title,
testSpecificMock: mockPhishingDetection,
dapp: true,
dappPaths: ['mock-page-with-disallowed-iframe'],
dappOptions: {
numberOfDapps: 2,
},
failOnConsoleError: false,
},
async ({ driver }) => {
await driver.navigate();
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
await driver.openNewPage(
`http://localhost:8080?extensionUrl=${driver.extensionUrl}`,
);
const iframe = await driver.findElement('iframe');
await driver.switchToFrame(iframe);
await driver.clickElement({
text: 'Open this warning in a new tab',
});
await driver.switchToWindowWithTitle('MetaMask Phishing Detection');
await driver.clickElement({
text: 'continuing at your own risk',
});
// Ensure we're not on the wallet home page
await driver.assertElementNotPresent('[data-testid="wallet-balance"]');
}, },
); );
}); });

@ -307,6 +307,10 @@ class Driver {
await this.driver.switchTo().window(handle); await this.driver.switchTo().window(handle);
} }
async switchToFrame(element) {
await this.driver.switchTo().frame(element);
}
async getAllWindowHandles() { async getAllWindowHandles() {
return await this.driver.getAllWindowHandles(); return await this.driver.getAllWindowHandles();
} }

@ -2848,7 +2848,7 @@
web3 "^0.20.7" web3 "^0.20.7"
web3-provider-engine "^16.0.3" web3-provider-engine "^16.0.3"
"@metamask/design-tokens@^1.6.5": "@metamask/design-tokens@^1.6.0", "@metamask/design-tokens@^1.6.5":
version "1.6.5" version "1.6.5"
resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.6.5.tgz#e585b67f73ce301e0218d98ba89e079f7e81c412" resolved "https://registry.yarnpkg.com/@metamask/design-tokens/-/design-tokens-1.6.5.tgz#e585b67f73ce301e0218d98ba89e079f7e81c412"
integrity sha512-5eCrUHXrIivXX1xx6kwNtM9s/ejhrPYSATSniFc7YKS9z+TkCK4/n52owOBnDIbrL8W3XxQIiaaqQAM+NQad4w== integrity sha512-5eCrUHXrIivXX1xx6kwNtM9s/ejhrPYSATSniFc7YKS9z+TkCK4/n52owOBnDIbrL8W3XxQIiaaqQAM+NQad4w==
@ -3006,6 +3006,18 @@
"@metamask/safe-event-emitter" "^2.0.0" "@metamask/safe-event-emitter" "^2.0.0"
through2 "^2.0.3" through2 "^2.0.3"
"@metamask/phishing-warning@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@metamask/phishing-warning/-/phishing-warning-1.1.0.tgz#0d5764327c413dd1900db786e295bd729530ca66"
integrity sha512-39N7lU9fdkXZKHn/hbkiEhv1oJ3mabPjYMk22582a7XzdBD5wgxDf5qHsXXQlOq+uI3lBU9VVKD7K7Z2lcgapw==
dependencies:
"@metamask/design-tokens" "^1.6.0"
"@metamask/post-message-stream" "^4.0.0"
globalthis "1.0.1"
obj-multiplex "^1.0.0"
pump "^3.0.0"
ses "0.12.4"
"@metamask/post-message-stream@4.0.0", "@metamask/post-message-stream@^4.0.0": "@metamask/post-message-stream@4.0.0", "@metamask/post-message-stream@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-4.0.0.tgz#72f120e562346ca86ccc9b3684023ad44265f0df" resolved "https://registry.yarnpkg.com/@metamask/post-message-stream/-/post-message-stream-4.0.0.tgz#72f120e562346ca86ccc9b3684023ad44265f0df"
@ -13219,7 +13231,7 @@ globals@^13.6.0, globals@^13.9.0:
dependencies: dependencies:
type-fest "^0.20.2" type-fest "^0.20.2"
globalthis@^1.0.0, globalthis@^1.0.1: globalthis@1.0.1, globalthis@^1.0.0, globalthis@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.1.tgz#40116f5d9c071f9e8fb0037654df1ab3a83b7ef9"
integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw== integrity sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==
@ -24264,7 +24276,7 @@ serve-static@1.14.1:
parseurl "~1.3.3" parseurl "~1.3.3"
send "0.17.1" send "0.17.1"
ses@^0.12.4: ses@0.12.4, ses@^0.12.4:
version "0.12.4" version "0.12.4"
resolved "https://registry.yarnpkg.com/ses/-/ses-0.12.4.tgz#f466f7199292b5c4454949c7d497f5569ade5805" resolved "https://registry.yarnpkg.com/ses/-/ses-0.12.4.tgz#f466f7199292b5c4454949c7d497f5569ade5805"
integrity sha512-qbtkhuuAXNXb390yiaNUdNvDg/QmX7W2cO+srIUJllINMYADc/8m0vt7DNBmq+rqOBRrjVRPPeyQq8ZTLK3Rmw== integrity sha512-qbtkhuuAXNXb390yiaNUdNvDg/QmX7W2cO+srIUJllINMYADc/8m0vt7DNBmq+rqOBRrjVRPPeyQq8ZTLK3Rmw==

Loading…
Cancel
Save