aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md7
-rw-r--r--app/manifest.json10
-rw-r--r--app/scripts/background.js26
-rw-r--r--app/scripts/blacklister.js17
-rw-r--r--app/scripts/controllers/infura.js14
-rw-r--r--app/scripts/controllers/transactions.js13
-rw-r--r--app/scripts/inpage.js1
-rw-r--r--app/scripts/lib/is-phish.js38
-rw-r--r--app/scripts/lib/nonce-tracker.js28
-rw-r--r--package.json1
-rw-r--r--test/unit/blacklister-test.js24
-rw-r--r--test/unit/nonce-tracker-test.js8
-rw-r--r--ui/app/reducers.js1
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
}