diff --git a/lib/package.json b/lib/package.json --- a/lib/package.json +++ b/lib/package.json @@ -42,6 +42,7 @@ "fast-json-stable-stringify": "^2.0.0", "file-type": "^12.3.0", "focus-trap-react": "^10.1.4", + "idna-uts46-hx": "^2.3.1", "invariant": "^2.2.4", "just-clone": "^3.2.1", "lodash": "^4.17.21", diff --git a/lib/shared/markdown.js b/lib/shared/markdown.js --- a/lib/shared/markdown.js +++ b/lib/shared/markdown.js @@ -353,6 +353,8 @@ ); } +const ensRegex: RegExp = /^.{3,}\.eth$/; + export { paragraphRegex, paragraphStripTrailingNewlineRegex, @@ -378,4 +380,5 @@ stripSpoilersFromNotifications, stripSpoilersFromMarkdownAST, parseChatMention, + ensRegex, }; diff --git a/lib/utils/ens-helpers.js b/lib/utils/ens-helpers.js --- a/lib/utils/ens-helpers.js +++ b/lib/utils/ens-helpers.js @@ -1,7 +1,10 @@ // @flow +import uts46 from 'idna-uts46-hx'; + import { ENSCache } from './ens-cache.js'; import { getETHAddressForUserInfo } from '../shared/account-utils.js'; +import { ensRegex } from '../shared/markdown.js'; type BaseUserInfo = { +username?: ?string, ... }; export type GetENSNames = ( @@ -70,4 +73,31 @@ }); } -export { getENSNames }; +function isValidENSName(name: string): boolean { + // While the Ethereum spec allows for more flexibility in ENS names + // (see https://eips.ethereum.org/EIPS/eip-137#name-syntax), we want to only + // perform lookups on names that adhere to two specific rules: + // 1. TLD should be .eth + // 2. SLD should be at least three characters in length + // Here, we enforce these rules and also use a library similar to the one + // recommended by the Ethereum spec to perform the 'heavy lifting' of + // making sure the name adheres to all of the specific limitations. + try { + // Our specific rules on TLDs and SLDs + const match = name.match(ensRegex); + if (!match) { + return false; + } + + // Ethereum spec guidelines (throws an error if invalid) + uts46.toAscii(name, { + transitional: false, + useStd3ASCII: true, + }); + return true; + } catch (e) { + return false; + } +} + +export { getENSNames, isValidENSName }; diff --git a/lib/utils/ens-helpers.test.js b/lib/utils/ens-helpers.test.js new file mode 100644 --- /dev/null +++ b/lib/utils/ens-helpers.test.js @@ -0,0 +1,54 @@ +// @flow + +import { isValidENSName } from './ens-helpers.js'; + +describe('it should correctly validate ENS names', () => { + it('should match all valid typical ENS names', () => { + expect(isValidENSName('foo.eth')).toBe(true); + expect(isValidENSName('jack.eth')).toBe(true); + expect(isValidENSName('thisuserhasareallylongname.eth')).toBe(true); + expect(isValidENSName('hello-world.eth')).toBe(true); + }); + + it('should match all valid ENS names with numbers', () => { + expect(isValidENSName('foo123.eth')).toBe(true); + expect(isValidENSName('123foo.eth')).toBe(true); + expect(isValidENSName('123foo123.eth')).toBe(true); + }); + + it('should match all valid ENS names with unicode characters', () => { + expect(isValidENSName('fΓΆΗ’.eth')).toBe(true); + expect(isValidENSName('hΓ«llΓΈ.eth')).toBe(true); + }); + + it('should match one-character emoji SLDs that are made up of 3 characters', () => { + expect(isValidENSName('πŸ’‚β€β™‚οΈ.eth')).toBe(true); + expect(isValidENSName('πŸ•΅οΈβ€β™‚οΈ.eth')).toBe(true); + expect(isValidENSName('πŸ‘¨β€πŸš€.eth')).toBe(true); + }); + + it('should not match one-character emoji SLDs that are made up of less than 3 characters', () => { + expect(isValidENSName('πŸ€.eth')).toBe(false); + expect(isValidENSName('πŸŽƒ.eth')).toBe(false); + }); + + it('should not match any SLDs less than 3 characters', () => { + expect(isValidENSName('fo.eth')).toBe(false); + expect(isValidENSName('f.eth')).toBe(false); + expect(isValidENSName('')).toBe(false); + expect(isValidENSName('a.eth')).toBe(false); + expect(isValidENSName('ΓΆ.eth')).toBe(false); + }); + + it('should not match any TLDs other than .eth', () => { + expect(isValidENSName('foo.com')).toBe(false); + expect(isValidENSName('foo.')).toBe(false); + expect(isValidENSName('foo')).toBe(false); + }); + + it('should not match any names with special characters', () => { + expect(isValidENSName('foo.eth!')).toBe(false); + expect(isValidENSName('foo.eth#')).toBe(false); + expect(isValidENSName('foo$.eth')).toBe(false); + }); +});