Skip to content

Commit fb540b0

Browse files
Gudahtttrn1ty409H
authored
Support multiple phishing configurations (#7079)
* Support multiple phishing configurations The phishing detector has been updated to support multiple phishing configurations. Both the configuration object and the result object have been updated to accommodate the need to identify the name of the config that the checked domain matched. Since the config and return value was already being changed, the nomenclature has been updated to replace `black/white` with `block/allow` as well, which is a change we have been meaning to make for some time. This change to both the configuration and result object applies only when the new configuration format is used. The old format preserves the old config and result value, making this a non-breaking change. The old configuration accepted three lists (`blacklist`, `whitelist`, and `fuzzylist`), and a `tolerance` value for the fuzzylist match. The new configuration is an array of objects rather than an object, to accommodate multiple configurations. Each configuration option accepts three lists (`blocklist`, `allowlist`, and `fuzzylist`), `tolerance` for the fuzzylist match, and two new properties: `name` and `version`. The `version` parameter was already used by the old configuration, but it was not required or used by the detector itself. It is now required with the new configuration, and it is returned with each match. The new `name` parameter describes which configuration matched the origin being checked (if any). This was critical for us because it allows us to direct the user to the appropriate place when they want to dispute a blocked site. The return value was updated to include the `name` and `version` parameters. The `type` was updated from `blacklist` to `blocklist` and from `whitelist` to `allowlist` as well. * v1.2.0 This release adds support for multiple phishing configurations, and includes changes to the configuration object and return value if an array of configuration values is passed to the phishing detector constructor. This is a non-breaking change because the old configuration format is still supported, and the return values remain the same if the old configuration format is used. Co-authored-by: Deven Blake <[email protected]> Co-authored-by: H <[email protected]>
1 parent 59aa4c1 commit fb540b0

File tree

3 files changed

+1118
-41
lines changed

3 files changed

+1118
-41
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "eth-phishing-detect",
3-
"version": "1.1.16",
3+
"version": "1.2.0",
44
"description": "Utility for detecting phishing domains targeting Ethereum users",
55
"main": "src/index.js",
66
"scripts": {

src/detector.js

+135-30
Original file line numberDiff line numberDiff line change
@@ -3,47 +3,112 @@ const DEFAULT_TOLERANCE = 3
33

44
class PhishingDetector {
55

6+
/**
7+
* Legacy phishing detector configuration.
8+
*
9+
* @typedef {object} LegacyPhishingDetectorConfiguration
10+
* @property {string[]} [whitelist] - Origins that should not be blocked.
11+
* @property {string[]} [blacklist] - Origins to block.
12+
* @property {string[]} [fuzzylist] - Origins of common phishing targets.
13+
* @property {number} [tolerance] - Tolerance to use for the fuzzylist levenshtein match.
14+
*/
15+
16+
/**
17+
* A configuration object for phishing detection.
18+
*
19+
* @typedef {object} PhishingDetectorConfiguration
20+
* @property {string[]} [allowlist] - Origins that should not be blocked.
21+
* @property {string[]} [blocklist] - Origins to block.
22+
* @property {string[]} [fuzzylist] - Origins of common phishing targets.
23+
* @property {string} name - The name of this configuration. Used to explain to users why a site is being blocked.
24+
* @property {number} [tolerance] - Tolerance to use for the fuzzylist levenshtein match.
25+
* @property {number} version - The current version of the configuration.
26+
*/
27+
28+
/**
29+
* Construct a phishing detector, which can check whether origins are known
30+
* to be malicious or similar to common phishing targets.
31+
*
32+
* A list of configurations is accepted. Each origin checked is processed
33+
* using each configuration in sequence, so the order defines which
34+
* configurations take precedence.
35+
*
36+
* @param {LegacyPhishingDetectorConfiguration | PhishingDetectorConfiguration[]} opts - Phishing detection options
37+
*/
638
constructor (opts) {
7-
this.whitelist = processDomainList(opts.whitelist || [])
8-
this.blacklist = processDomainList(opts.blacklist || [])
9-
this.fuzzylist = processDomainList(opts.fuzzylist || [])
10-
this.tolerance = ('tolerance' in opts) ? opts.tolerance : DEFAULT_TOLERANCE
39+
// recommended configuration
40+
if (Array.isArray(opts)) {
41+
this.configs = processConfigs(opts)
42+
this.legacyConfig = false
43+
// legacy configuration
44+
} else {
45+
this.configs = [{
46+
allowlist: processDomainList(opts.whitelist || []),
47+
blocklist: processDomainList(opts.blacklist || []),
48+
fuzzylist: processDomainList(opts.fuzzylist || []),
49+
tolerance: ('tolerance' in opts) ? opts.tolerance : DEFAULT_TOLERANCE
50+
}]
51+
this.legacyConfig = true
52+
}
1153
}
1254

13-
check (domain) {
14-
let fqdn = domain.substring(domain.length - 1) === "."
55+
check(domain) {
56+
const result = this._check(domain)
57+
58+
if (this.legacyConfig) {
59+
let legacyType = result.type;
60+
if (legacyType === 'allowlist') {
61+
legacyType = 'whitelist'
62+
} else if (legacyType === 'blocklist') {
63+
legacyType = 'blacklist'
64+
}
65+
return {
66+
match: result.match,
67+
result: result.result,
68+
type: legacyType,
69+
}
70+
}
71+
return result
72+
}
73+
74+
_check (domain) {
75+
let fqdn = domain.substring(domain.length - 1) === "."
1576
? domain.slice(0, -1)
1677
: domain;
1778

1879
const source = domainToParts(fqdn)
1980

20-
// if source matches whitelist domain (or subdomain thereof), PASS
21-
const whitelistMatch = matchPartsAgainstList(source, this.whitelist)
22-
if (whitelistMatch) return { type: 'whitelist', result: false }
23-
24-
// if source matches blacklist domain (or subdomain thereof), FAIL
25-
const blacklistMatch = matchPartsAgainstList(source, this.blacklist)
26-
if (blacklistMatch) return { type: 'blacklist', result: true }
27-
28-
if (this.tolerance > 0) {
29-
// check if near-match of whitelist domain, FAIL
30-
let fuzzyForm = domainPartsToFuzzyForm(source)
31-
// strip www
32-
fuzzyForm = fuzzyForm.replace('www.', '')
33-
// check against fuzzylist
34-
const levenshteinMatched = this.fuzzylist.find((targetParts) => {
35-
const fuzzyTarget = domainPartsToFuzzyForm(targetParts)
36-
const distance = levenshtein.get(fuzzyForm, fuzzyTarget)
37-
return distance <= this.tolerance
38-
})
39-
if (levenshteinMatched) {
40-
const match = domainPartsToDomain(levenshteinMatched)
41-
return { type: 'fuzzy', result: true, match }
81+
for (const { allowlist, name, version } of this.configs) {
82+
// if source matches whitelist domain (or subdomain thereof), PASS
83+
const whitelistMatch = matchPartsAgainstList(source, allowlist)
84+
if (whitelistMatch) return { name, result: false, type: 'allowlist', version }
85+
}
86+
87+
for (const { blocklist, fuzzylist, name, tolerance, version } of this.configs) {
88+
// if source matches blacklist domain (or subdomain thereof), FAIL
89+
const blacklistMatch = matchPartsAgainstList(source, blocklist)
90+
if (blacklistMatch) return { name, result: true, type: 'blocklist', version }
91+
92+
if (tolerance > 0) {
93+
// check if near-match of whitelist domain, FAIL
94+
let fuzzyForm = domainPartsToFuzzyForm(source)
95+
// strip www
96+
fuzzyForm = fuzzyForm.replace('www.', '')
97+
// check against fuzzylist
98+
const levenshteinMatched = fuzzylist.find((targetParts) => {
99+
const fuzzyTarget = domainPartsToFuzzyForm(targetParts)
100+
const distance = levenshtein.get(fuzzyForm, fuzzyTarget)
101+
return distance <= tolerance
102+
})
103+
if (levenshteinMatched) {
104+
const match = domainPartsToDomain(levenshteinMatched)
105+
return { name, match, result: true, type: 'fuzzy', version }
106+
}
42107
}
43108
}
44109

45110
// matched nothing, PASS
46-
return { type: 'all', result: false }
111+
return { result: false, type: 'all' }
47112
}
48113

49114
}
@@ -52,12 +117,52 @@ module.exports = PhishingDetector
52117

53118
// util
54119

120+
function processConfigs(configs = []) {
121+
return configs.map((config) => {
122+
validateConfig(config)
123+
return Object.assign({}, config, {
124+
allowlist: processDomainList(config.allowlist || []),
125+
blocklist: processDomainList(config.blocklist || []),
126+
fuzzylist: processDomainList(config.fuzzylist || []),
127+
tolerance: ('tolerance' in config) ? config.tolerance : DEFAULT_TOLERANCE
128+
})
129+
});
130+
}
131+
132+
function validateConfig(config) {
133+
if (config === null || typeof config !== 'object') {
134+
throw new Error('Invalid config')
135+
}
136+
137+
if (config.tolerance && !config.fuzzylist) {
138+
throw new Error('Fuzzylist tolerance provided without fuzzylist')
139+
}
140+
141+
if (
142+
typeof config.name !== 'string' ||
143+
config.name === ''
144+
) {
145+
throw new Error("Invalid config parameter: 'name'")
146+
}
147+
148+
if (
149+
!['number', 'string'].includes(typeof config.version) ||
150+
config.version === ''
151+
) {
152+
throw new Error("Invalid config parameter: 'version'")
153+
}
154+
}
155+
55156
function processDomainList (list) {
56157
return list.map(domainToParts)
57158
}
58159

59160
function domainToParts (domain) {
161+
try {
60162
return domain.split('.').reverse()
163+
} catch (e) {
164+
throw new Error(JSON.stringify(domain))
165+
}
61166
}
62167

63168
function domainPartsToDomain(domainParts) {
@@ -80,4 +185,4 @@ function matchPartsAgainstList(source, list) {
80185
// source matches target or (is deeper subdomain)
81186
return target.every((part, index) => source[index] === part)
82187
})
83-
}
188+
}

0 commit comments

Comments
 (0)