diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5d635d3..66c95a0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +- Continuously update blacklist for known phishing sites in background. +- Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. + ## 3.9.2 2017-7-26 - Fix bugs that could sometimes result in failed transactions after switching networks. diff --git a/app/manifest.json b/app/manifest.json index 55e1eb5b1..591a07d0d 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -55,8 +55,12 @@ }, { "run_at": "document_start", - "matches": ["http://*/*", "https://*/*"], - "js": ["scripts/blacklister.js"] + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": ["scripts/blacklister.js"], + "all_frames": true } ], "permissions": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index 7e8f9172f..bc0fbdc37 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -11,6 +11,7 @@ const NotificationManager = require('./lib/notification-manager.js') const MetamaskController = require('./metamask-controller') const extension = require('extensionizer') const firstTimeState = require('./first-time-state') +const isPhish = require('./lib/is-phish') const STORAGE_KEY = 'metamask-config' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' @@ -90,8 +91,11 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - const name = remotePort.name - var isMetaMaskInternalProcess = name === 'popup' || name === 'notification' || name === 'ui' + if (remotePort.name === 'blacklister') { + return checkBlacklist(remotePort) + } + + var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' var portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup @@ -136,6 +140,27 @@ function setupController (initState) { return Promise.resolve() } +// Listen for new pages and return if blacklisted: +function checkBlacklist (port) { + const handler = handleNewPageLoad.bind(null, port) + port.onMessage.addListener(handler) + setTimeout(() => { + port.onMessage.removeListener(handler) + }, 30000) +} + +function handleNewPageLoad (port, message) { + const { pageLoaded } = message + if (!pageLoaded || !global.metamaskController) return + + const state = global.metamaskController.getState() + const updatedBlacklist = state.blacklist + + if (isPhish({ updatedBlacklist, hostname: pageLoaded })) { + port.postMessage({ 'blacklist': pageLoaded }) + } +} + // // Etc... // diff --git a/app/scripts/blacklister.js b/app/scripts/blacklister.js index 9337599cc..37751b595 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -1,41 +1,14 @@ -const levenshtein = require('fast-levenshtein') -const blacklistedMetaMaskDomains = ['metamask.com'] -const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json').concat(blacklistedMetaMaskDomains) -const whitelistedMetaMaskDomains = ['metamask.io', 'www.metamask.io'] -const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json').concat(whitelistedMetaMaskDomains) -const LEVENSHTEIN_TOLERANCE = 4 -const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] +const extension = require('extensionizer') +var port = extension.runtime.connect({name: 'blacklister'}) +port.postMessage({ 'pageLoaded': window.location.hostname }) +port.onMessage.addListener(redirectIfBlacklisted) -// credit to @sogoiii and @409H for their help! -// Return a boolean on whether or not a phish is detected. -function isPhish(hostname) { - var strCurrentTab = hostname - - // check if the domain is part of the whitelist. - if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } - - // check if the domain is part of the blacklist. - var isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) - - // check for similar values. - var levenshteinMatched = false - var levenshteinForm = strCurrentTab.replace(/\./g, '') - LEVENSHTEIN_CHECKS.forEach((element) => { - if (levenshtein.get(element, levenshteinForm) < LEVENSHTEIN_TOLERANCE) { - levenshteinMatched = true - } - }) - - return isBlacklisted || levenshteinMatched -} - -window.addEventListener('load', function () { - var hostnameToCheck = window.location.hostname - if (isPhish(hostnameToCheck)) { - // redirect to our phishing warning page. +function redirectIfBlacklisted (response) { + const { blacklist } = response + const host = window.location.hostname + if (blacklist && blacklist === host) { window.location.href = 'https://metamask.io/phishing.html' } -}) +} -module.exports = isPhish diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index b34b0bc03..97b2ab7e3 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,5 +1,6 @@ const ObservableStore = require('obs-store') const extend = require('xtend') +const recentBlacklist = require('etheraddresslookup/blacklists/domains.json') // every ten minutes const POLLING_INTERVAL = 300000 @@ -9,6 +10,7 @@ class InfuraController { constructor (opts = {}) { const initState = extend({ infuraNetworkStatus: {}, + blacklist: recentBlacklist, }, opts.initState) this.store = new ObservableStore(initState) } @@ -30,12 +32,24 @@ class InfuraController { }) } + updateLocalBlacklist () { + return fetch('https://api.infura.io/v1/blacklist') + .then(response => response.json()) + .then((parsedResponse) => { + this.store.updateState({ + blacklist: parsedResponse, + }) + return parsedResponse + }) + } + scheduleInfuraNetworkCheck () { if (this.conversionInterval) { clearInterval(this.conversionInterval) } this.conversionInterval = setInterval(() => { this.checkInfuraNetworkStatus() + this.updateLocalBlacklist() }, POLLING_INTERVAL) } } diff --git a/app/scripts/inpage.js b/app/scripts/inpage.js index ec764535e..9e98c044b 100644 --- a/app/scripts/inpage.js +++ b/app/scripts/inpage.js @@ -65,3 +65,4 @@ function restoreContextAfterImports () { console.warn('MetaMask - global.define could not be overwritten.') } } + diff --git a/app/scripts/lib/is-phish.js b/app/scripts/lib/is-phish.js new file mode 100644 index 000000000..68c09e4ac --- /dev/null +++ b/app/scripts/lib/is-phish.js @@ -0,0 +1,38 @@ +const levenshtein = require('fast-levenshtein') +const blacklistedMetaMaskDomains = ['metamask.com'] +let blacklistedDomains = require('etheraddresslookup/blacklists/domains.json').concat(blacklistedMetaMaskDomains) +const whitelistedMetaMaskDomains = ['metamask.io', 'www.metamask.io'] +const whitelistedDomains = require('etheraddresslookup/whitelists/domains.json').concat(whitelistedMetaMaskDomains) +const LEVENSHTEIN_TOLERANCE = 4 +const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] + + +// credit to @sogoiii and @409H for their help! +// Return a boolean on whether or not a phish is detected. +function isPhish({ hostname, updatedBlacklist = null }) { + var strCurrentTab = hostname + + // check if the domain is part of the whitelist. + if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } + + // Allow updating of blacklist: + if (updatedBlacklist) { + blacklistedDomains = blacklistedDomains.concat(updatedBlacklist) + } + + // check if the domain is part of the blacklist. + const isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) + + // check for similar values. + let levenshteinMatched = false + var levenshteinForm = strCurrentTab.replace(/\./g, '') + LEVENSHTEIN_CHECKS.forEach((element) => { + if (levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE) { + levenshteinMatched = true + } + }) + + return isBlacklisted || levenshteinMatched +} + +module.exports = isPhish diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js index d9290795c..1badc2c8f 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/blacklister-test.js @@ -1,24 +1,24 @@ const assert = require('assert') -const Blacklister = require('../../app/scripts/blacklister') - +const isPhish = require('../../app/scripts/lib/is-phish') describe('blacklister', function () { describe('#isPhish', function () { it('should not flag whitelisted values', function () { - var result = Blacklister('www.metamask.io') + var result = isPhish({ hostname: 'www.metamask.io' }) assert(!result) }) it('should flag explicit values', function () { - var result = Blacklister('metamask.com') + var result = isPhish({ hostname: 'metamask.com' }) assert(result) }) it('should flag levenshtein values', function () { - var result = Blacklister('metmask.io') + var result = isPhish({ hostname: 'metmask.com' }) assert(result) }) it('should not flag not-even-close values', function () { - var result = Blacklister('example.com') + var result = isPhish({ hostname: 'example.com' }) assert(!result) }) }) }) +