diff options
-rw-r--r-- | CHANGELOG.md | 7 | ||||
-rw-r--r-- | app/manifest.json | 10 | ||||
-rw-r--r-- | app/scripts/background.js | 26 | ||||
-rw-r--r-- | app/scripts/blacklister.js | 17 | ||||
-rw-r--r-- | app/scripts/controllers/infura.js | 14 | ||||
-rw-r--r-- | app/scripts/controllers/transactions.js | 13 | ||||
-rw-r--r-- | app/scripts/inpage.js | 1 | ||||
-rw-r--r-- | app/scripts/lib/is-phish.js | 38 | ||||
-rw-r--r-- | app/scripts/lib/nonce-tracker.js | 28 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | test/unit/blacklister-test.js | 24 | ||||
-rw-r--r-- | test/unit/nonce-tracker-test.js | 8 | ||||
-rw-r--r-- | ui/app/reducers.js | 1 |
13 files changed, 163 insertions, 25 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index eeeda9d68..66c95a0c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,14 @@ ## 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. - Include stack traces in txMeta's to better understand the life cycle of transactions +- Enhance blacklister functionality to include levenshtein logic. (credit to @sogoiii and @409H for their help!) ## 3.9.1 2017-7-19 diff --git a/app/manifest.json b/app/manifest.json index eadd99590..591a07d0d 100644 --- a/app/manifest.json +++ b/app/manifest.json @@ -1,7 +1,7 @@ { "name": "MetaMask", "short_name": "Metamask", - "version": "3.9.1", + "version": "3.9.2", "manifest_version": 2, "author": "https://metamask.io", "description": "Ethereum Browser Extension", @@ -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 e8987394f..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,6 +91,10 @@ 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) if (isMetaMaskInternalProcess) { @@ -135,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 a45265a75..37751b595 100644 --- a/app/scripts/blacklister.js +++ b/app/scripts/blacklister.js @@ -1,13 +1,14 @@ -const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json')
+const extension = require('extensionizer')
-function detectBlacklistedDomain() {
- var strCurrentTab = window.location.hostname
- if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) {
+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'
}
}
-window.addEventListener('load', function() {
- detectBlacklistedDomain()
-})
-
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/controllers/transactions.js b/app/scripts/controllers/transactions.js index 43dfb9360..8855dfd5b 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -25,7 +25,6 @@ module.exports = class TransactionController extends EventEmitter { this.blockTracker = opts.blockTracker this.nonceTracker = new NonceTracker({ provider: this.provider, - blockTracker: this.provider._blockTracker, getPendingTransactions: (address) => { return this.getFilteredTxList({ from: address, @@ -104,8 +103,16 @@ module.exports = class TransactionController extends EventEmitter { } updateTx (txMeta) { + // create txMeta snapshot for history 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) + const txId = txMeta.id const txList = this.getFullTxList() const index = txList.findIndex(txData => txData.id === txId) @@ -192,8 +199,12 @@ module.exports = class TransactionController extends EventEmitter { // get next nonce const txMeta = this.getTx(txId) const fromAddress = txMeta.txParams.from + // wait for a nonce nonceLock = await this.nonceTracker.getNonceLock(fromAddress) + // add nonce to txParams txMeta.txParams.nonce = nonceLock.nextNonce + // add nonce debugging information to txMeta + txMeta.nonceDetails = nonceLock.nonceDetails this.updateTx(txMeta) // sign transaction const rawTx = await this.signTransaction(txId) 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/app/scripts/lib/nonce-tracker.js b/app/scripts/lib/nonce-tracker.js index b76dac4e8..8328e81ec 100644 --- a/app/scripts/lib/nonce-tracker.js +++ b/app/scripts/lib/nonce-tracker.js @@ -4,8 +4,8 @@ const Mutex = require('await-semaphore').Mutex class NonceTracker { - constructor ({ blockTracker, provider, getPendingTransactions }) { - this.blockTracker = blockTracker + constructor ({ provider, getPendingTransactions }) { + this.provider = provider this.ethQuery = new EthQuery(provider) this.getPendingTransactions = getPendingTransactions this.lockMap = {} @@ -31,21 +31,25 @@ class NonceTracker { const currentBlock = await this._getCurrentBlock() const pendingTransactions = this.getPendingTransactions(address) const pendingCount = pendingTransactions.length - assert(Number.isInteger(pendingCount), 'nonce-tracker - pendingCount is an integer') + assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`) const baseCountHex = await this._getTxCount(address, currentBlock) const baseCount = parseInt(baseCountHex, 16) - assert(Number.isInteger(baseCount), 'nonce-tracker - baseCount is an integer') + assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`) const nextNonce = baseCount + pendingCount - assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') - // return next nonce and release cb - return { nextNonce, releaseLock } + assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`) + // collect the numbers used to calculate the nonce for debugging + const blockNumber = currentBlock.number + const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount } + // return nonce and release cb + return { nextNonce, nonceDetails, releaseLock } } async _getCurrentBlock () { - const currentBlock = this.blockTracker.getCurrentBlock() + const blockTracker = this._getBlockTracker() + const currentBlock = blockTracker.getCurrentBlock() if (currentBlock) return currentBlock return await Promise((reject, resolve) => { - this.blockTracker.once('latest', resolve) + blockTracker.once('latest', resolve) }) } @@ -79,6 +83,12 @@ class NonceTracker { return mutex } + // this is a hotfix for the fact that the blockTracker will + // change when the network changes + _getBlockTracker () { + return this.provider._blockTracker + } + } module.exports = NonceTracker diff --git a/package.json b/package.json index 94232d46d..9171bc206 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "express": "^4.14.0", "extension-link-enabler": "^1.0.0", "extensionizer": "^1.0.0", + "fast-levenshtein": "^2.0.6", "gulp-eslint": "^2.0.0", "hat": "0.0.3", "idb-global": "^1.0.0", diff --git a/test/unit/blacklister-test.js b/test/unit/blacklister-test.js new file mode 100644 index 000000000..1badc2c8f --- /dev/null +++ b/test/unit/blacklister-test.js @@ -0,0 +1,24 @@ +const assert = require('assert') +const isPhish = require('../../app/scripts/lib/is-phish') + +describe('blacklister', function () { + describe('#isPhish', function () { + it('should not flag whitelisted values', function () { + var result = isPhish({ hostname: 'www.metamask.io' }) + assert(!result) + }) + it('should flag explicit values', function () { + var result = isPhish({ hostname: 'metamask.com' }) + assert(result) + }) + it('should flag levenshtein values', function () { + var result = isPhish({ hostname: 'metmask.com' }) + assert(result) + }) + it('should not flag not-even-close values', function () { + var result = isPhish({ hostname: 'example.com' }) + assert(!result) + }) + }) +}) + diff --git a/test/unit/nonce-tracker-test.js b/test/unit/nonce-tracker-test.js index 16cd6d008..b0283e159 100644 --- a/test/unit/nonce-tracker-test.js +++ b/test/unit/nonce-tracker-test.js @@ -18,11 +18,13 @@ describe('Nonce Tracker', function () { getPendingTransactions = () => pendingTxs - provider = { sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) } } - nonceTracker = new NonceTracker({ - blockTracker: { + provider = { + sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) }, + _blockTracker: { getCurrentBlock: () => '0x11b568', }, + } + nonceTracker = new NonceTracker({ provider, getPendingTransactions, }) diff --git a/ui/app/reducers.js b/ui/app/reducers.js index 11efca529..36045772f 100644 --- a/ui/app/reducers.js +++ b/ui/app/reducers.js @@ -43,7 +43,6 @@ function rootReducer (state, action) { window.logState = function () { var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) - console.log(stateString) return stateString } |