Merge branch 'master' into transactionControllerRefractor

feature/default_network_editable
frankiebee 7 years ago
commit 25cffd21f8
  1. 7
      CHANGELOG.md
  2. 10
      app/manifest.json
  3. 26
      app/scripts/background.js
  4. 17
      app/scripts/blacklister.js
  5. 14
      app/scripts/controllers/infura.js
  6. 13
      app/scripts/controllers/transactions.js
  7. 1
      app/scripts/inpage.js
  8. 38
      app/scripts/lib/is-phish.js
  9. 28
      app/scripts/lib/nonce-tracker.js
  10. 1
      package.json
  11. 24
      test/unit/blacklister-test.js
  12. 8
      test/unit/nonce-tracker-test.js
  13. 1
      ui/app/reducers.js

@ -2,7 +2,14 @@
## Current Master ## 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 - 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 ## 3.9.1 2017-7-19

@ -1,7 +1,7 @@
{ {
"name": "MetaMask", "name": "MetaMask",
"short_name": "Metamask", "short_name": "Metamask",
"version": "3.9.1", "version": "3.9.2",
"manifest_version": 2, "manifest_version": 2,
"author": "https://metamask.io", "author": "https://metamask.io",
"description": "Ethereum Browser Extension", "description": "Ethereum Browser Extension",
@ -55,8 +55,12 @@
}, },
{ {
"run_at": "document_start", "run_at": "document_start",
"matches": ["http://*/*", "https://*/*"], "matches": [
"js": ["scripts/blacklister.js"] "http://*/*",
"https://*/*"
],
"js": ["scripts/blacklister.js"],
"all_frames": true
} }
], ],
"permissions": [ "permissions": [

@ -11,6 +11,7 @@ const NotificationManager = require('./lib/notification-manager.js')
const MetamaskController = require('./metamask-controller') const MetamaskController = require('./metamask-controller')
const extension = require('extensionizer') const extension = require('extensionizer')
const firstTimeState = require('./first-time-state') const firstTimeState = require('./first-time-state')
const isPhish = require('./lib/is-phish')
const STORAGE_KEY = 'metamask-config' const STORAGE_KEY = 'metamask-config'
const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG' const METAMASK_DEBUG = 'GULP_METAMASK_DEBUG'
@ -90,6 +91,10 @@ function setupController (initState) {
extension.runtime.onConnect.addListener(connectRemote) extension.runtime.onConnect.addListener(connectRemote)
function connectRemote (remotePort) { function connectRemote (remotePort) {
if (remotePort.name === 'blacklister') {
return checkBlacklist(remotePort)
}
var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification' var isMetaMaskInternalProcess = remotePort.name === 'popup' || remotePort.name === 'notification'
var portStream = new PortStream(remotePort) var portStream = new PortStream(remotePort)
if (isMetaMaskInternalProcess) { if (isMetaMaskInternalProcess) {
@ -135,6 +140,27 @@ function setupController (initState) {
return Promise.resolve() 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... // Etc...
// //

@ -1,13 +1,14 @@
const blacklistedDomains = require('etheraddresslookup/blacklists/domains.json') const extension = require('extensionizer')
function detectBlacklistedDomain() { var port = extension.runtime.connect({name: 'blacklister'})
var strCurrentTab = window.location.hostname port.postMessage({ 'pageLoaded': window.location.hostname })
if (blacklistedDomains && blacklistedDomains.includes(strCurrentTab)) { 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.location.href = 'https://metamask.io/phishing.html'
} }
} }
window.addEventListener('load', function() {
detectBlacklistedDomain()
})

@ -1,5 +1,6 @@
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const extend = require('xtend') const extend = require('xtend')
const recentBlacklist = require('etheraddresslookup/blacklists/domains.json')
// every ten minutes // every ten minutes
const POLLING_INTERVAL = 300000 const POLLING_INTERVAL = 300000
@ -9,6 +10,7 @@ class InfuraController {
constructor (opts = {}) { constructor (opts = {}) {
const initState = extend({ const initState = extend({
infuraNetworkStatus: {}, infuraNetworkStatus: {},
blacklist: recentBlacklist,
}, opts.initState) }, opts.initState)
this.store = new ObservableStore(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 () { scheduleInfuraNetworkCheck () {
if (this.conversionInterval) { if (this.conversionInterval) {
clearInterval(this.conversionInterval) clearInterval(this.conversionInterval)
} }
this.conversionInterval = setInterval(() => { this.conversionInterval = setInterval(() => {
this.checkInfuraNetworkStatus() this.checkInfuraNetworkStatus()
this.updateLocalBlacklist()
}, POLLING_INTERVAL) }, POLLING_INTERVAL)
} }
} }

@ -25,7 +25,6 @@ module.exports = class TransactionController extends EventEmitter {
this.blockTracker = opts.blockTracker this.blockTracker = opts.blockTracker
this.nonceTracker = new NonceTracker({ this.nonceTracker = new NonceTracker({
provider: this.provider, provider: this.provider,
blockTracker: this.provider._blockTracker,
getPendingTransactions: (address) => { getPendingTransactions: (address) => {
return this.getFilteredTxList({ return this.getFilteredTxList({
from: address, from: address,
@ -104,8 +103,16 @@ module.exports = class TransactionController extends EventEmitter {
} }
updateTx (txMeta) { updateTx (txMeta) {
// create txMeta snapshot for history
const txMetaForHistory = clone(txMeta) 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() txMetaForHistory.stack = getStack()
// add snapshot to tx history
if (!txMeta.history) txMeta.history = []
txMeta.history.push(txMetaForHistory)
const txId = txMeta.id const txId = txMeta.id
const txList = this.getFullTxList() const txList = this.getFullTxList()
const index = txList.findIndex(txData => txData.id === txId) const index = txList.findIndex(txData => txData.id === txId)
@ -192,8 +199,12 @@ module.exports = class TransactionController extends EventEmitter {
// get next nonce // get next nonce
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
const fromAddress = txMeta.txParams.from const fromAddress = txMeta.txParams.from
// wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams
txMeta.txParams.nonce = nonceLock.nextNonce txMeta.txParams.nonce = nonceLock.nextNonce
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
this.updateTx(txMeta) this.updateTx(txMeta)
// sign transaction // sign transaction
const rawTx = await this.signTransaction(txId) const rawTx = await this.signTransaction(txId)

@ -65,3 +65,4 @@ function restoreContextAfterImports () {
console.warn('MetaMask - global.define could not be overwritten.') console.warn('MetaMask - global.define could not be overwritten.')
} }
} }

@ -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

@ -4,8 +4,8 @@ const Mutex = require('await-semaphore').Mutex
class NonceTracker { class NonceTracker {
constructor ({ blockTracker, provider, getPendingTransactions }) { constructor ({ provider, getPendingTransactions }) {
this.blockTracker = blockTracker this.provider = provider
this.ethQuery = new EthQuery(provider) this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions this.getPendingTransactions = getPendingTransactions
this.lockMap = {} this.lockMap = {}
@ -31,21 +31,25 @@ class NonceTracker {
const currentBlock = await this._getCurrentBlock() const currentBlock = await this._getCurrentBlock()
const pendingTransactions = this.getPendingTransactions(address) const pendingTransactions = this.getPendingTransactions(address)
const pendingCount = pendingTransactions.length 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 baseCountHex = await this._getTxCount(address, currentBlock)
const baseCount = parseInt(baseCountHex, 16) 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 const nextNonce = baseCount + pendingCount
assert(Number.isInteger(nextNonce), 'nonce-tracker - nextNonce is an integer') assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// return next nonce and release cb // collect the numbers used to calculate the nonce for debugging
return { nextNonce, releaseLock } const blockNumber = currentBlock.number
const nonceDetails = { blockNumber, baseCount, baseCountHex, pendingCount }
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
} }
async _getCurrentBlock () { async _getCurrentBlock () {
const currentBlock = this.blockTracker.getCurrentBlock() const blockTracker = this._getBlockTracker()
const currentBlock = blockTracker.getCurrentBlock()
if (currentBlock) return currentBlock if (currentBlock) return currentBlock
return await Promise((reject, resolve) => { return await Promise((reject, resolve) => {
this.blockTracker.once('latest', resolve) blockTracker.once('latest', resolve)
}) })
} }
@ -79,6 +83,12 @@ class NonceTracker {
return mutex 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 module.exports = NonceTracker

@ -81,6 +81,7 @@
"express": "^4.14.0", "express": "^4.14.0",
"extension-link-enabler": "^1.0.0", "extension-link-enabler": "^1.0.0",
"extensionizer": "^1.0.0", "extensionizer": "^1.0.0",
"fast-levenshtein": "^2.0.6",
"gulp-eslint": "^2.0.0", "gulp-eslint": "^2.0.0",
"hat": "0.0.3", "hat": "0.0.3",
"idb-global": "^1.0.0", "idb-global": "^1.0.0",

@ -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)
})
})
})

@ -18,11 +18,13 @@ describe('Nonce Tracker', function () {
getPendingTransactions = () => pendingTxs getPendingTransactions = () => pendingTxs
provider = { sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) } } provider = {
nonceTracker = new NonceTracker({ sendAsync: (_, cb) => { cb(undefined, {result: '0x0'}) },
blockTracker: { _blockTracker: {
getCurrentBlock: () => '0x11b568', getCurrentBlock: () => '0x11b568',
}, },
}
nonceTracker = new NonceTracker({
provider, provider,
getPendingTransactions, getPendingTransactions,
}) })

@ -43,7 +43,6 @@ function rootReducer (state, action) {
window.logState = function () { window.logState = function () {
var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2) var stateString = JSON.stringify(window.METAMASK_CACHED_LOG_STATE, removeSeedWords, 2)
console.log(stateString)
return stateString return stateString
} }

Loading…
Cancel
Save