From 9d345e744d61d94879f3c01a263aa9cf73afa36b Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 16:58:40 -0700 Subject: [PATCH 01/15] blacklist - clearer test format --- test/unit/blacklister-test.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js index 1badc2c8f..47e9b3c6b 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/blacklister-test.js @@ -5,19 +5,19 @@ describe('blacklister', function () { describe('#isPhish', function () { it('should not flag whitelisted values', function () { var result = isPhish({ hostname: 'www.metamask.io' }) - assert(!result) + assert.equal(result, false) }) it('should flag explicit values', function () { var result = isPhish({ hostname: 'metamask.com' }) - assert(result) + assert.equal(result, true) }) it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.com' }) - assert(result) + var result = isPhish({ hostname: 'metmask.io' }) + assert.equal(result, true) }) it('should not flag not-even-close values', function () { var result = isPhish({ hostname: 'example.com' }) - assert(!result) + assert.equal(result, false) }) }) }) From 9eb13aee0046405bb304a2e31dbf7712e8b1da21 Mon Sep 17 00:00:00 2001 From: kumavis Date: Tue, 1 Aug 2017 17:05:36 -0700 Subject: [PATCH 02/15] blacklist - add tests for metamask subdomains --- test/unit/blacklister-test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js index 47e9b3c6b..ce110491c 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/blacklister-test.js @@ -19,6 +19,18 @@ describe('blacklister', function () { var result = isPhish({ hostname: 'example.com' }) assert.equal(result, false) }) + it('should not flag the ropsten faucet domains', function () { + var result = isPhish({ hostname: 'faucet.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + var result = isPhish({ hostname: 'zero.metamask.io' }) + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) + assert.equal(result, false) + }) }) }) From aea5735b29c87d0a9aad3bfa86d854ed9b20bdf7 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 14:25:28 -0700 Subject: [PATCH 03/15] obj-multiplex - missing name error + prefer const over var --- app/scripts/lib/obj-multiplex.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/scripts/lib/obj-multiplex.js b/app/scripts/lib/obj-multiplex.js index bd114c394..0034febe0 100644 --- a/app/scripts/lib/obj-multiplex.js +++ b/app/scripts/lib/obj-multiplex.js @@ -5,12 +5,16 @@ module.exports = ObjectMultiplex function ObjectMultiplex (opts) { opts = opts || {} // create multiplexer - var mx = through.obj(function (chunk, enc, cb) { - var name = chunk.name - var data = chunk.data - var substream = mx.streams[name] + const mx = through.obj(function (chunk, enc, cb) { + const name = chunk.name + const data = chunk.data + if (!name) { + console.warn(`ObjectMultiplex - Malformed chunk without name "${chunk}"`) + return cb() + } + const substream = mx.streams[name] if (!substream) { - console.warn(`orphaned data for stream "${name}"`) + console.warn(`ObjectMultiplex - orphaned data for stream "${name}"`) } else { if (substream.push) substream.push(data) } @@ -19,7 +23,7 @@ function ObjectMultiplex (opts) { mx.streams = {} // create substreams mx.createStream = function (name) { - var substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { + const substream = mx.streams[name] = through.obj(function (chunk, enc, cb) { mx.push({ name: name, data: chunk, From ecaa235b5e3331defab75dad72593951fdf37790 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 14:26:10 -0700 Subject: [PATCH 04/15] phishing detection - move phishing detection into contentscript and metamask controller --- app/manifest.json | 9 ----- app/scripts/background.js | 34 +++---------------- app/scripts/blacklister.js | 14 -------- app/scripts/contentscript.js | 22 ++++++++---- app/scripts/lib/inpage-provider.js | 3 ++ app/scripts/lib/is-phish.js | 6 ++-- app/scripts/metamask-controller.js | 26 ++++++++++++-- gulpfile.js | 1 - package.json | 2 +- ...ter-test.js => phishing-detection-test.js} | 2 +- 10 files changed, 52 insertions(+), 67 deletions(-) delete mode 100644 app/scripts/blacklister.js rename test/unit/{blacklister-test.js => phishing-detection-test.js} (96%) diff --git a/app/manifest.json b/app/manifest.json index 591a07d0d..1eaf6f26a 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -52,15 +52,6 @@ ], "run_at": "document_start", "all_frames": true - }, - { - "run_at": "document_start", - "matches": [ - "http://*/*", - "https://*/*" - ], - "js": ["scripts/blacklister.js"], - "all_frames": true } ], "permissions": [ diff --git a/app/scripts/background.js b/app/scripts/background.js index bc0fbdc37..a235afff3 100644 --- a/app/scripts/background.js +++ b/app/scripts/background.js @@ -11,7 +11,6 @@ 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' @@ -91,16 +90,12 @@ function setupController (initState) { extension.runtime.onConnect.addListener(connectRemote) function connectRemote (remotePort) { - if (remotePort.name === 'blacklister') { - return checkBlacklist(remotePort) - } - - var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' - var portStream = new PortStream(remotePort) + const isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' + const portStream = new PortStream(remotePort) if (isMetaMaskInternalProcess) { // communication with popup popupIsOpen = popupIsOpen || (remotePort.name === 'popup') - controller.setupTrustedCommunication(portStream, 'MetaMask', remotePort.name) + controller.setupTrustedCommunication(portStream, 'MetaMask') // record popup as closed if (remotePort.name === 'popup') { endOfStream(portStream, () => { @@ -109,7 +104,7 @@ function setupController (initState) { } } else { // communication with page - var originDomain = urlUtil.parse(remotePort.sender.url).hostname + const originDomain = urlUtil.parse(remotePort.sender.url).hostname controller.setupUntrustedCommunication(portStream, originDomain) } } @@ -140,27 +135,6 @@ 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 deleted file mode 100644 index 37751b595..000000000 --- a/app/scripts/blacklister.js +++ /dev/null @@ -1,14 +0,0 @@ -const extension = require('extensionizer') - -var port = extension.runtime.connect({name: 'blacklister'}) -port.postMessage({ 'pageLoaded': window.location.hostname }) -port.onMessage.addListener(redirectIfBlacklisted) - -function redirectIfBlacklisted (response) { - const { blacklist } = response - const host = window.location.hostname - if (blacklist && blacklist === host) { - window.location.href = 'https://metamask.io/phishing.html' - } -} - diff --git a/app/scripts/contentscript.js b/app/scripts/contentscript.js index 291b922e8..6fde0edcd 100644 --- a/app/scripts/contentscript.js +++ b/app/scripts/contentscript.js @@ -37,28 +37,33 @@ function setupInjection () { function setupStreams () { // setup communication to page and plugin - var pageStream = new LocalMessageDuplexStream({ + const pageStream = new LocalMessageDuplexStream({ name: 'contentscript', target: 'inpage', }) pageStream.on('error', console.error) - var pluginPort = extension.runtime.connect({name: 'contentscript'}) - var pluginStream = new PortStream(pluginPort) + const pluginPort = extension.runtime.connect({ name: 'contentscript' }) + const pluginStream = new PortStream(pluginPort) pluginStream.on('error', console.error) // forward communication plugin->inpage pageStream.pipe(pluginStream).pipe(pageStream) // setup local multistream channels - var mx = ObjectMultiplex() + const mx = ObjectMultiplex() mx.on('error', console.error) mx.pipe(pageStream).pipe(mx) + mx.pipe(pluginStream).pipe(mx) // connect ping stream - var pongStream = new PongStream({ objectMode: true }) + const pongStream = new PongStream({ objectMode: true }) pongStream.pipe(mx.createStream('pingpong')).pipe(pongStream) - // ignore unused channels (handled by background) + // connect phishing warning stream + const phishingStream = mx.createStream('phishing') + phishingStream.once('data', redirectToPhishingWarning) + + // ignore unused channels (handled by background, inpage) mx.ignoreStream('provider') mx.ignoreStream('publicConfig') } @@ -88,3 +93,8 @@ function suffixCheck () { } return true } + +function redirectToPhishingWarning () { + console.log('MetaMask - redirecting to phishing warning') + window.location.href = 'https://metamask.io/phishing.html' +} diff --git a/app/scripts/lib/inpage-provider.js b/app/scripts/lib/inpage-provider.js index 8b8623974..fd032a673 100644 --- a/app/scripts/lib/inpage-provider.js +++ b/app/scripts/lib/inpage-provider.js @@ -26,6 +26,9 @@ function MetamaskInpageProvider (connectionStream) { (err) => logStreamDisconnectWarning('MetaMask PublicConfigStore', err) ) + // ignore phishing warning message (handled elsewhere) + multiStream.ignoreStream('phishing') + // connect to async provider const asyncProvider = self.asyncProvider = new StreamProvider() pipe( diff --git a/app/scripts/lib/is-phish.js b/app/scripts/lib/is-phish.js index 68c09e4ac..21c68b0d6 100644 --- a/app/scripts/lib/is-phish.js +++ b/app/scripts/lib/is-phish.js @@ -9,15 +9,15 @@ const LEVENSHTEIN_CHECKS = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'meta // credit to @sogoiii and @409H for their help! // Return a boolean on whether or not a phish is detected. -function isPhish({ hostname, updatedBlacklist = null }) { +function isPhish({ hostname, blacklist }) { 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) + if (blacklist) { + blacklistedDomains = blacklistedDomains.concat(blacklist) } // check if the domain is part of the blacklist. diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 11dcde2c1..28c35a13d 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -23,6 +23,7 @@ const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') +const checkForPhishing = require('./lib/is-phish') const debounce = require('debounce') const version = require('../manifest.json').version @@ -326,8 +327,15 @@ module.exports = class MetamaskController extends EventEmitter { } setupUntrustedCommunication (connectionStream, originDomain) { + // Check if new connection is blacklisted + if (this.isHostBlacklisted(originDomain)) { + console.log('MetaMask - sending phishing warning for', originDomain) + this.sendPhishingWarning(connectionStream, originDomain) + return + } + // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupProviderConnection(mx.createStream('provider'), originDomain) this.setupPublicConfig(mx.createStream('publicConfig')) @@ -335,12 +343,26 @@ module.exports = class MetamaskController extends EventEmitter { setupTrustedCommunication (connectionStream, originDomain) { // setup multiplexing - var mx = setupMultiplex(connectionStream) + const mx = setupMultiplex(connectionStream) // connect features this.setupControllerConnection(mx.createStream('controller')) this.setupProviderConnection(mx.createStream('provider'), originDomain) } + // Check if a domain is on our blacklist + isHostBlacklisted (hostname) { + if (!hostname) return false + const { blacklist } = this.getState().blacklist + return checkForPhishing({ blacklist, hostname }) + } + + sendPhishingWarning (connectionStream, hostname) { + const mx = setupMultiplex(connectionStream) + const phishingStream = mx.createStream('phishing') + // phishingStream.write(true) + phishingStream.write({ hostname }) + } + setupControllerConnection (outStream) { const api = this.getApi() const dnode = Dnode(api) diff --git a/gulpfile.js b/gulpfile.js index 53de7a7d9..cc723704a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -172,7 +172,6 @@ gulp.task('default', ['lint'], function () { const jsFiles = [ 'inpage', 'contentscript', - 'blacklister', 'background', 'popup', ] diff --git a/package.json b/package.json index 10afc8228..40bc78c6f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,7 @@ "sw-stream": "^2.0.0", "textarea-caret": "^3.0.1", "three.js": "^0.73.2", - "through2": "^2.0.1", + "through2": "^2.0.3", "valid-url": "^1.0.9", "vreme": "^3.0.2", "web3": "0.19.1", diff --git a/test/unit/blacklister-test.js b/test/unit/phishing-detection-test.js similarity index 96% rename from test/unit/blacklister-test.js rename to test/unit/phishing-detection-test.js index ce110491c..affcb16c2 100644 --- a/test/unit/blacklister-test.js +++ b/test/unit/phishing-detection-test.js @@ -1,7 +1,7 @@ const assert = require('assert') const isPhish = require('../../app/scripts/lib/is-phish') -describe('blacklister', function () { +describe('phishing detection test', function () { describe('#isPhish', function () { it('should not flag whitelisted values', function () { var result = isPhish({ hostname: 'www.metamask.io' }) From 8c6f01b91094564df59d6d95b6f43b811e711824 Mon Sep 17 00:00:00 2001 From: kumavis Date: Wed, 2 Aug 2017 15:54:59 -0700 Subject: [PATCH 05/15] blacklist controller - breakout from metamask and infura controllers --- app/scripts/controllers/blacklist.js | 50 ++++++++++++++++++++++++++ app/scripts/controllers/infura.js | 16 +-------- app/scripts/lib/is-phish.js | 29 ++++----------- app/scripts/metamask-controller.js | 19 +++++----- test/unit/blacklist-controller-test.js | 41 +++++++++++++++++++++ test/unit/phishing-detection-test.js | 36 ------------------- 6 files changed, 108 insertions(+), 83 deletions(-) create mode 100644 app/scripts/controllers/blacklist.js create mode 100644 test/unit/blacklist-controller-test.js delete mode 100644 test/unit/phishing-detection-test.js diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js new file mode 100644 index 000000000..11e26d5b2 --- /dev/null +++ b/app/scripts/controllers/blacklist.js @@ -0,0 +1,50 @@ +const ObservableStore = require('obs-store') +const extend = require('xtend') +const communityBlacklistedDomains = require('etheraddresslookup/blacklists/domains.json') +const communityWhitelistedDomains = require('etheraddresslookup/whitelists/domains.json') +const checkForPhishing = require('../lib/is-phish') + +// compute phishing lists +const PHISHING_BLACKLIST = communityBlacklistedDomains.concat(['metamask.com']) +const PHISHING_WHITELIST = communityWhitelistedDomains.concat(['metamask.io', 'www.metamask.io']) +const PHISHING_FUZZYLIST = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] +// every ten minutes +const POLLING_INTERVAL = 10 * 60 * 1000 + +class BlacklistController { + + constructor (opts = {}) { + const initState = extend({ + phishing: PHISHING_BLACKLIST, + }, opts.initState) + this.store = new ObservableStore(initState) + // polling references + this._phishingUpdateIntervalRef = null + } + + // + // PUBLIC METHODS + // + + checkForPhishing (hostname) { + if (!hostname) return false + const { blacklist } = this.store.getState() + return checkForPhishing({ hostname, blacklist, whitelist: PHISHING_WHITELIST, fuzzylist: PHISHING_FUZZYLIST }) + } + + async updatePhishingList () { + const response = await fetch('https://api.infura.io/v1/blacklist') + const phishing = await response.json() + this.store.updateState({ phishing }) + return phishing + } + + scheduleUpdates () { + if (this._phishingUpdateIntervalRef) return + this._phishingUpdateIntervalRef = setInterval(() => { + this.updatePhishingList() + }, POLLING_INTERVAL) + } +} + +module.exports = BlacklistController diff --git a/app/scripts/controllers/infura.js b/app/scripts/controllers/infura.js index 97b2ab7e3..10adb1004 100644 --- a/app/scripts/controllers/infura.js +++ b/app/scripts/controllers/infura.js @@ -1,16 +1,14 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const recentBlacklist = require('etheraddresslookup/blacklists/domains.json') // every ten minutes -const POLLING_INTERVAL = 300000 +const POLLING_INTERVAL = 10 * 60 * 1000 class InfuraController { constructor (opts = {}) { const initState = extend({ infuraNetworkStatus: {}, - blacklist: recentBlacklist, }, opts.initState) this.store = new ObservableStore(initState) } @@ -32,24 +30,12 @@ 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/lib/is-phish.js b/app/scripts/lib/is-phish.js index 21c68b0d6..ce51c353d 100644 --- a/app/scripts/lib/is-phish.js +++ b/app/scripts/lib/is-phish.js @@ -1,38 +1,23 @@ 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, blacklist }) { - var strCurrentTab = hostname +function isPhish({ hostname, blacklist, whitelist, fuzzylist }) { // check if the domain is part of the whitelist. - if (whitelistedDomains && whitelistedDomains.includes(strCurrentTab)) { return false } - - // Allow updating of blacklist: - if (blacklist) { - blacklistedDomains = blacklistedDomains.concat(blacklist) - } + if (whitelist && whitelist.includes(hostname)) return false // check if the domain is part of the blacklist. - const isBlacklisted = blacklistedDomains && blacklistedDomains.includes(strCurrentTab) + if (blacklist && blacklist.includes(hostname)) return true // 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 - } + const levenshteinForm = hostname.replace(/\./g, '') + const levenshteinMatched = fuzzylist.some((element) => { + return levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE }) - return isBlacklisted || levenshteinMatched + return levenshteinMatched } module.exports = isPhish diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index 28c35a13d..6d6cb85ab 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -16,6 +16,7 @@ const NoticeController = require('./notice-controller') const ShapeShiftController = require('./controllers/shapeshift') const AddressBookController = require('./controllers/address-book') const InfuraController = require('./controllers/infura') +const BlacklistController = require('./controllers/blacklist') const MessageManager = require('./lib/message-manager') const PersonalMessageManager = require('./lib/personal-message-manager') const TransactionController = require('./controllers/transactions') @@ -23,7 +24,6 @@ const ConfigManager = require('./lib/config-manager') const nodeify = require('./lib/nodeify') const accountImporter = require('./account-import-strategies') const getBuyEthUrl = require('./lib/buy-eth-url') -const checkForPhishing = require('./lib/is-phish') const debounce = require('debounce') const version = require('../manifest.json').version @@ -70,6 +70,10 @@ module.exports = class MetamaskController extends EventEmitter { }) this.infuraController.scheduleInfuraNetworkCheck() + this.blacklistController = new BlacklistController({ + initState: initState.BlacklistController, + }) + this.blacklistController.scheduleUpdates() // rpc provider this.provider = this.initializeProvider() @@ -152,6 +156,9 @@ module.exports = class MetamaskController extends EventEmitter { this.networkController.store.subscribe((state) => { this.store.updateState({ NetworkController: state }) }) + this.blacklistController.store.subscribe((state) => { + this.store.updateState({ BlacklistController: state }) + }) this.infuraController.store.subscribe((state) => { this.store.updateState({ InfuraController: state }) }) @@ -328,7 +335,7 @@ module.exports = class MetamaskController extends EventEmitter { setupUntrustedCommunication (connectionStream, originDomain) { // Check if new connection is blacklisted - if (this.isHostBlacklisted(originDomain)) { + if (this.blacklistController.checkForPhishing(originDomain)) { console.log('MetaMask - sending phishing warning for', originDomain) this.sendPhishingWarning(connectionStream, originDomain) return @@ -349,17 +356,9 @@ module.exports = class MetamaskController extends EventEmitter { this.setupProviderConnection(mx.createStream('provider'), originDomain) } - // Check if a domain is on our blacklist - isHostBlacklisted (hostname) { - if (!hostname) return false - const { blacklist } = this.getState().blacklist - return checkForPhishing({ blacklist, hostname }) - } - sendPhishingWarning (connectionStream, hostname) { const mx = setupMultiplex(connectionStream) const phishingStream = mx.createStream('phishing') - // phishingStream.write(true) phishingStream.write({ hostname }) } diff --git a/test/unit/blacklist-controller-test.js b/test/unit/blacklist-controller-test.js new file mode 100644 index 000000000..a9260466f --- /dev/null +++ b/test/unit/blacklist-controller-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const BlacklistController = require('../../app/scripts/controllers/blacklist') + +describe('blacklist controller', function () { + let blacklistController + + before(() => { + blacklistController = new BlacklistController() + }) + + describe('checkForPhishing', function () { + it('should not flag whitelisted values', function () { + const result = blacklistController.checkForPhishing('www.metamask.io') + assert.equal(result, false) + }) + it('should flag explicit values', function () { + const result = blacklistController.checkForPhishing('metamask.com') + assert.equal(result, true) + }) + it('should flag levenshtein values', function () { + const result = blacklistController.checkForPhishing('metmask.io') + assert.equal(result, true) + }) + it('should not flag not-even-close values', function () { + const result = blacklistController.checkForPhishing('example.com') + assert.equal(result, false) + }) + it('should not flag the ropsten faucet domains', function () { + const result = blacklistController.checkForPhishing('faucet.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara domain', function () { + const result = blacklistController.checkForPhishing('zero.metamask.io') + assert.equal(result, false) + }) + it('should not flag the mascara-faucet domain', function () { + const result = blacklistController.checkForPhishing('zero-faucet.metamask.io') + assert.equal(result, false) + }) + }) +}) \ No newline at end of file diff --git a/test/unit/phishing-detection-test.js b/test/unit/phishing-detection-test.js deleted file mode 100644 index affcb16c2..000000000 --- a/test/unit/phishing-detection-test.js +++ /dev/null @@ -1,36 +0,0 @@ -const assert = require('assert') -const isPhish = require('../../app/scripts/lib/is-phish') - -describe('phishing detection test', function () { - describe('#isPhish', function () { - it('should not flag whitelisted values', function () { - var result = isPhish({ hostname: 'www.metamask.io' }) - assert.equal(result, false) - }) - it('should flag explicit values', function () { - var result = isPhish({ hostname: 'metamask.com' }) - assert.equal(result, true) - }) - it('should flag levenshtein values', function () { - var result = isPhish({ hostname: 'metmask.io' }) - assert.equal(result, true) - }) - it('should not flag not-even-close values', function () { - var result = isPhish({ hostname: 'example.com' }) - assert.equal(result, false) - }) - it('should not flag the ropsten faucet domains', function () { - var result = isPhish({ hostname: 'faucet.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara domain', function () { - var result = isPhish({ hostname: 'zero.metamask.io' }) - assert.equal(result, false) - }) - it('should not flag the mascara-faucet domain', function () { - var result = isPhish({ hostname: 'zero-faucet.metamask.io' }) - assert.equal(result, false) - }) - }) -}) - From dce593fd7bbe1848b8744e3911f24d9ac2fa1bf7 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 3 Aug 2017 15:39:55 -0400 Subject: [PATCH 06/15] remove stack from txs --- app/scripts/controllers/transactions.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 720323e41..308d43cb0 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -5,7 +5,6 @@ const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const EthQuery = require('ethjs-query') const TxProviderUtil = require('../lib/tx-utils') -const getStack = require('../lib/util').getStack const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -105,8 +104,6 @@ module.exports = class TransactionController extends EventEmitter { const txMetaForHistory = clone(txMeta) // dont include previous history in this snapshot delete txMetaForHistory.history - // add stack to help understand why tx was updated - txMetaForHistory.stack = getStack() // add snapshot to tx history if (!txMeta.history) txMeta.history = [] txMeta.history.push(txMetaForHistory) From d4877cb4e2580db565686e7c736cd3716dc6e02d Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 14:25:02 -0700 Subject: [PATCH 07/15] blacklist - use module eth-phishing-detect --- app/scripts/controllers/blacklist.js | 28 ++++++++++++++++++---------- app/scripts/lib/is-phish.js | 23 ----------------------- package.json | 4 ++-- 3 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 app/scripts/lib/is-phish.js diff --git a/app/scripts/controllers/blacklist.js b/app/scripts/controllers/blacklist.js index 11e26d5b2..7e01fa386 100644 --- a/app/scripts/controllers/blacklist.js +++ b/app/scripts/controllers/blacklist.js @@ -1,13 +1,9 @@ const ObservableStore = require('obs-store') const extend = require('xtend') -const communityBlacklistedDomains = require('etheraddresslookup/blacklists/domains.json') -const communityWhitelistedDomains = require('etheraddresslookup/whitelists/domains.json') -const checkForPhishing = require('../lib/is-phish') +const PhishingDetector = require('eth-phishing-detect/src/detector') // compute phishing lists -const PHISHING_BLACKLIST = communityBlacklistedDomains.concat(['metamask.com']) -const PHISHING_WHITELIST = communityWhitelistedDomains.concat(['metamask.io', 'www.metamask.io']) -const PHISHING_FUZZYLIST = ['myetherwallet', 'myetheroll', 'ledgerwallet', 'metamask'] +const PHISHING_DETECTION_CONFIG = require('eth-phishing-detect/src/config.json') // every ten minutes const POLLING_INTERVAL = 10 * 60 * 1000 @@ -15,9 +11,12 @@ class BlacklistController { constructor (opts = {}) { const initState = extend({ - phishing: PHISHING_BLACKLIST, + phishing: PHISHING_DETECTION_CONFIG, }, opts.initState) this.store = new ObservableStore(initState) + // phishing detector + this._phishingDetector = null + this._setupPhishingDetector(initState.phishing) // polling references this._phishingUpdateIntervalRef = null } @@ -28,14 +27,15 @@ class BlacklistController { checkForPhishing (hostname) { if (!hostname) return false - const { blacklist } = this.store.getState() - return checkForPhishing({ hostname, blacklist, whitelist: PHISHING_WHITELIST, fuzzylist: PHISHING_FUZZYLIST }) + const { result } = this._phishingDetector.check(hostname) + return result } async updatePhishingList () { - const response = await fetch('https://api.infura.io/v1/blacklist') + const response = await fetch('https://api.infura.io/v2/blacklist') const phishing = await response.json() this.store.updateState({ phishing }) + this._setupPhishingDetector(phishing) return phishing } @@ -45,6 +45,14 @@ class BlacklistController { this.updatePhishingList() }, POLLING_INTERVAL) } + + // + // PRIVATE METHODS + // + + _setupPhishingDetector (config) { + this._phishingDetector = new PhishingDetector(config) + } } module.exports = BlacklistController diff --git a/app/scripts/lib/is-phish.js b/app/scripts/lib/is-phish.js deleted file mode 100644 index ce51c353d..000000000 --- a/app/scripts/lib/is-phish.js +++ /dev/null @@ -1,23 +0,0 @@ -const levenshtein = require('fast-levenshtein') -const LEVENSHTEIN_TOLERANCE = 4 - -// credit to @sogoiii and @409H for their help! -// Return a boolean on whether or not a phish is detected. -function isPhish({ hostname, blacklist, whitelist, fuzzylist }) { - - // check if the domain is part of the whitelist. - if (whitelist && whitelist.includes(hostname)) return false - - // check if the domain is part of the blacklist. - if (blacklist && blacklist.includes(hostname)) return true - - // check for similar values. - const levenshteinForm = hostname.replace(/\./g, '') - const levenshteinMatched = fuzzylist.some((element) => { - return levenshtein.get(element, levenshteinForm) <= LEVENSHTEIN_TOLERANCE - }) - - return levenshteinMatched -} - -module.exports = isPhish diff --git a/package.json b/package.json index a086af29d..a85da614c 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "start": "npm run dev", "dev": "gulp dev --debug", "disc": "gulp disc --debug", - "clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/etheraddresslookup", + "clear": "rm -rf node_modules/eth-contract-metadata && rm -rf node_modules/eth-phishing-detect", "dist": "npm run clear && npm install && gulp dist", "test": "npm run lint && npm run test-unit && npm run test-integration", "test-unit": "METAMASK_ENV=test mocha --require test/helper.js --recursive \"test/unit/**/*.js\"", @@ -68,11 +68,11 @@ "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", + "eth-phishing-detect": "^1.0.2", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", "eth-token-tracker": "^1.1.2", - "etheraddresslookup": "github:409H/EtherAddressLookup", "ethereumjs-tx": "^1.3.0", "ethereumjs-util": "ethereumjs/ethereumjs-util#ac5d0908536b447083ea422b435da27f26615de9", "ethereumjs-wallet": "^0.6.0", From 026f8592d8e3c5af976062bca9b93e936e218fa5 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 15:59:01 -0700 Subject: [PATCH 08/15] deps - bump eth-detect-phishing --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a85da614c..4f702e048 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "eth-bin-to-ops": "^1.0.1", "eth-contract-metadata": "^1.1.4", "eth-hd-keyring": "^1.1.1", - "eth-phishing-detect": "^1.0.2", + "eth-phishing-detect": "^1.1.0", "eth-query": "^2.1.2", "eth-sig-util": "^1.2.2", "eth-simple-keyring": "^1.1.1", From 10c6aeb2f8da24cb31f50de638aaa62acfbb7fca Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 17:01:09 -0700 Subject: [PATCH 09/15] v3.9.3 --- CHANGELOG.md | 3 +++ app/manifest.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66c95a0c3..7a607c19a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Current Master +## 3.9.3 2017-8-03 + +- Add support for EGO uport token - Continuously update blacklist for known phishing sites in background. - Automatically detect suspicious URLs too similar to common phishing targets, and blacklist them. diff --git a/app/manifest.json b/app/manifest.json index 1eaf6f26a..6c02120c1 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.2", + "version": "3.9.3", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", From 7de58c8709dd78e7088210e2c6bc5df79e008538 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 3 Aug 2017 21:20:01 -0400 Subject: [PATCH 10/15] fix cancelTransaction not reciving a callback --- app/scripts/lib/nodeify.js | 3 ++- ui/app/actions.js | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/scripts/lib/nodeify.js b/app/scripts/lib/nodeify.js index 299bfe624..832d6c6d3 100644 --- a/app/scripts/lib/nodeify.js +++ b/app/scripts/lib/nodeify.js @@ -1,9 +1,10 @@ const promiseToCallback = require('promise-to-callback') -module.exports = function(fn, context) { +module.exports = function nodeify (fn, context) { return function(){ const args = [].slice.call(arguments) const callback = args.pop() + if (typeof callback !== 'function') throw new Error('callback is not a function') promiseToCallback(fn.apply(context, args))(callback) } } diff --git a/ui/app/actions.js b/ui/app/actions.js index d99291e46..8ff8bbbdd 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -462,9 +462,12 @@ function cancelPersonalMsg (msgData) { } function cancelTx (txData) { - log.debug(`background.cancelTransaction`) - background.cancelTransaction(txData.id) - return actions.completedTx(txData.id) + return (dispatch) => { + log.debug(`background.cancelTransaction`) + background.cancelTransaction(txData.id, () => { + dispatch(actions.completedTx(txData.id)) + }) + } } // From a8966c4a4fa03ba717ddf09c8b8cd6a19d97dfe3 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 3 Aug 2017 22:02:31 -0400 Subject: [PATCH 11/15] fix test --- test/unit/actions/tx_test.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 0ea1bfdc7..4561082fd 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -45,11 +45,16 @@ describe('tx confirmation screen', function () { before(function (done) { actions._setBackgroundConnection({ approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId) { /* noop */ }, + cancelTransaction (txId, cb) { cb() }, clearSeedWordCache (cb) { cb() }, }) + // var dispatchExpect = sinon.mock() + // dispatchExpect.once() - const action = actions.cancelTx({value: firstTxId}) + let action + actions.cancelTx({value: firstTxId})((dispatchAction) => { + action = dispatchAction + }) result = reducers(initialState, action) done() }) From b955b5a89a9ed7fd74cfec84cbf4b3623e4661e4 Mon Sep 17 00:00:00 2001 From: frankiebee Date: Thu, 3 Aug 2017 22:10:33 -0400 Subject: [PATCH 12/15] add test for no callback error --- test/unit/nodeify-test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index 06241334d..f4fe320ca 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -17,4 +17,15 @@ describe('nodeify', function () { done() }) }) + + it('should throw if the last argument is not a function', function (done) { + var nodified = nodeify(obj.promiseFunc, obj) + try { + nodified('baz') + done(new Error('should have thrown if the last argument is not a function')) + } catch (err) { + if (err.message === 'callback is not a function') done() + else done(err) + } + }) }) From 500e7d1f04e72558fa8eb4aee60c24e5d7b71a94 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 20:52:09 -0700 Subject: [PATCH 13/15] nodeify - test - syntax nitpick --- test/unit/nodeify-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/nodeify-test.js b/test/unit/nodeify-test.js index f4fe320ca..537dae605 100644 --- a/test/unit/nodeify-test.js +++ b/test/unit/nodeify-test.js @@ -19,13 +19,13 @@ describe('nodeify', function () { }) it('should throw if the last argument is not a function', function (done) { - var nodified = nodeify(obj.promiseFunc, obj) + const nodified = nodeify(obj.promiseFunc, obj) try { nodified('baz') done(new Error('should have thrown if the last argument is not a function')) } catch (err) { - if (err.message === 'callback is not a function') done() - else done(err) + assert.equal(err.message, 'callback is not a function') + done() } }) }) From efb7543c6dba8a0a37044c1799a9aa906425ebd6 Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 20:53:47 -0700 Subject: [PATCH 14/15] test - actions - remove commented code --- test/unit/actions/tx_test.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 4561082fd..267cfee97 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -48,8 +48,6 @@ describe('tx confirmation screen', function () { cancelTransaction (txId, cb) { cb() }, clearSeedWordCache (cb) { cb() }, }) - // var dispatchExpect = sinon.mock() - // dispatchExpect.once() let action actions.cancelTx({value: firstTxId})((dispatchAction) => { From d526f68c8e7d613ad79ec9383d6709651e5af77e Mon Sep 17 00:00:00 2001 From: kumavis Date: Thu, 3 Aug 2017 20:56:53 -0700 Subject: [PATCH 15/15] test - actions - tx - fix async test --- test/unit/actions/tx_test.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index 267cfee97..67c72e9a5 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -49,12 +49,11 @@ describe('tx confirmation screen', function () { clearSeedWordCache (cb) { cb() }, }) - let action - actions.cancelTx({value: firstTxId})((dispatchAction) => { - action = dispatchAction + actions.cancelTx({value: firstTxId})((action) => { + result = reducers(initialState, action) + done() }) - result = reducers(initialState, action) - done() + }) it('should transition to the account detail view', function () {