Warn users when an ENS name contains 'confusable' characters (#9187)

* Add warning system for 'confusable' ENS names (#9129)

Uses unicode.org's TR39 confusables.txt to display a warning when
'confusable' unicode points are detected.

Currently only the `AddRecipient` component has been updated, but the new
`Confusable` component could be used elsewhere

The new `unicode-confusables` dependency adds close to 100KB to the
bundle size, and around 30KB when gzipped.

Adds 'tag' prop to the tooltop-v2 component

Use $Red-500 for confusable ens warning

Lint Tooltip component

Update copy for confusing ENS domain warning.

* Fix prop type

Co-authored-by: Mark Stacey <markjstacey@gmail.com>
feature/default_network_editable
ty 4 years ago committed by GitHub
parent caa32d87fb
commit b04120dd0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/_locales/en/messages.json
  2. 1
      package.json
  3. 39
      ui/app/components/ui/confusable/confusable.component.js
  4. 1
      ui/app/components/ui/confusable/index.js
  5. 5
      ui/app/components/ui/confusable/index.scss
  6. 26
      ui/app/components/ui/confusable/test/confusable.component.test.js
  7. 12
      ui/app/components/ui/tooltip/tooltip.js
  8. 1
      ui/app/components/ui/ui-components.scss
  9. 3
      ui/app/pages/send/send-content/add-recipient/add-recipient.component.js
  10. 6
      ui/app/pages/send/send-content/add-recipient/add-recipient.js
  11. 13
      ui/app/pages/send/send-content/add-recipient/tests/add-recipient-utils.test.js
  12. 2
      ui/app/pages/send/send.constants.js
  13. 5
      yarn.lock

@ -301,6 +301,15 @@
"confirmed": { "confirmed": {
"message": "Confirmed" "message": "Confirmed"
}, },
"confusableUnicode": {
"message": "'$1' is similar to '$2'."
},
"confusableZeroWidthUnicode": {
"message": "Zero-width character found."
},
"confusingEnsDomain": {
"message": "We have detected a confusable character in the ENS name. Check the ENS name to avoid a potential scam."
},
"congratulations": { "congratulations": {
"message": "Congratulations" "message": "Congratulations"
}, },

@ -177,6 +177,7 @@
"single-call-balance-checker-abi": "^1.0.0", "single-call-balance-checker-abi": "^1.0.0",
"swappable-obj-proxy": "^1.1.0", "swappable-obj-proxy": "^1.1.0",
"textarea-caret": "^3.0.1", "textarea-caret": "^3.0.1",
"unicode-confusables": "^0.1.1",
"valid-url": "^1.0.9", "valid-url": "^1.0.9",
"web3": "^0.20.7", "web3": "^0.20.7",
"web3-stream-provider": "^4.0.0" "web3-stream-provider": "^4.0.0"

@ -0,0 +1,39 @@
import React, { useMemo } from 'react';
import PropTypes from 'prop-types';
import { confusables } from 'unicode-confusables';
import Tooltip from '../tooltip';
import { useI18nContext } from '../../../hooks/useI18nContext';
const Confusable = ({ input }) => {
const t = useI18nContext();
const confusableData = useMemo(() => {
return confusables(input);
}, [input]);
return confusableData.map(({ point, similarTo }, index) => {
const zeroWidth = similarTo === '';
if (similarTo === undefined) {
return point;
}
return (
<Tooltip
key={index.toString()}
tag="span"
position="top"
title={
zeroWidth
? t('confusableZeroWidthUnicode')
: t('confusableUnicode', [point, similarTo])
}
>
<span className="confusable__point">{zeroWidth ? '?' : point}</span>
</Tooltip>
);
});
};
Confusable.propTypes = {
input: PropTypes.string.isRequired,
};
export default Confusable;

@ -0,0 +1 @@
export { default } from './confusable.component';

@ -0,0 +1,5 @@
.confusable {
&__point {
color: $Red-500;
}
}

@ -0,0 +1,26 @@
import assert from 'assert';
import React from 'react';
import { shallow } from 'enzyme';
import Confusable from '../confusable.component';
describe('Confusable component', function () {
it('should detect zero-width unicode', function () {
const wrapper = shallow(<Confusable input="vitalik.eth" />);
assert.ok(wrapper.find('.confusable__point').length === 1);
});
it('should detect homoglyphic unicode points', function () {
const wrapper = shallow(<Confusable input="faceboоk.eth" />);
assert.ok(wrapper.find('.confusable__point').length === 1);
});
it('should detect multiple homoglyphic unicode points', function () {
const wrapper = shallow(<Confusable input="ѕсоре.eth" />);
assert.ok(wrapper.find('.confusable__point').length === 5);
});
it('should not detect emoji', function () {
const wrapper = shallow(<Confusable input="👻.eth" />);
assert.ok(wrapper.find('.confusable__point').length === 0);
});
});

@ -17,6 +17,7 @@ export default class Tooltip extends PureComponent {
trigger: 'mouseenter focus', trigger: 'mouseenter focus',
wrapperClassName: undefined, wrapperClassName: undefined,
theme: '', theme: '',
tag: 'div',
}; };
static propTypes = { static propTypes = {
@ -36,6 +37,7 @@ export default class Tooltip extends PureComponent {
style: PropTypes.object, style: PropTypes.object,
theme: PropTypes.string, theme: PropTypes.string,
tabIndex: PropTypes.number, tabIndex: PropTypes.number,
tag: PropTypes.string,
}; };
render() { render() {
@ -56,14 +58,16 @@ export default class Tooltip extends PureComponent {
style, style,
theme, theme,
tabIndex, tabIndex,
tag,
} = this.props; } = this.props;
if (!title && !html) { if (!title && !html) {
return <div className={wrapperClassName}>{children}</div>; return <div className={wrapperClassName}>{children}</div>;
} }
return ( return React.createElement(
<div className={wrapperClassName}> tag,
{ className: wrapperClassName },
<ReactTippy <ReactTippy
arrow={arrow} arrow={arrow}
className={containerClassName} className={containerClassName}
@ -80,10 +84,10 @@ export default class Tooltip extends PureComponent {
trigger={trigger} trigger={trigger}
theme={theme} theme={theme}
tabIndex={tabIndex || 0} tabIndex={tabIndex || 0}
tag={tag}
> >
{children} {children}
</ReactTippy> </ReactTippy>,
</div>
); );
} }
} }

@ -12,6 +12,7 @@
@import 'chip/chip'; @import 'chip/chip';
@import 'circle-icon/index'; @import 'circle-icon/index';
@import 'color-indicator/color-indicator'; @import 'color-indicator/color-indicator';
@import 'confusable/index';
@import 'currency-display/index'; @import 'currency-display/index';
@import 'currency-input/index'; @import 'currency-input/index';
@import 'definition-list/definition-list'; @import 'definition-list/definition-list';

@ -8,6 +8,7 @@ import ContactList from '../../../../components/app/contact-list';
import RecipientGroup from '../../../../components/app/contact-list/recipient-group/recipient-group.component'; import RecipientGroup from '../../../../components/app/contact-list/recipient-group/recipient-group.component';
import { ellipsify } from '../../send.utils'; import { ellipsify } from '../../send.utils';
import Button from '../../../../components/ui/button'; import Button from '../../../../components/ui/button';
import Confusable from '../../../../components/ui/confusable';
export default class AddRecipient extends Component { export default class AddRecipient extends Component {
static propTypes = { static propTypes = {
@ -128,7 +129,7 @@ export default class AddRecipient extends Component {
<Identicon address={address} diameter={28} /> <Identicon address={address} diameter={28} />
<div className="send__select-recipient-wrapper__group-item__content"> <div className="send__select-recipient-wrapper__group-item__content">
<div className="send__select-recipient-wrapper__group-item__title"> <div className="send__select-recipient-wrapper__group-item__title">
{name || ellipsify(address)} {name ? <Confusable input={name} /> : ellipsify(address)}
</div> </div>
{name && ( {name && (
<div className="send__select-recipient-wrapper__group-item__subtitle"> <div className="send__select-recipient-wrapper__group-item__subtitle">

@ -1,16 +1,19 @@
import ethUtil from 'ethereumjs-util'; import ethUtil from 'ethereumjs-util';
import contractMap from '@metamask/contract-metadata'; import contractMap from '@metamask/contract-metadata';
import { isConfusing } from 'unicode-confusables';
import { import {
REQUIRED_ERROR, REQUIRED_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
KNOWN_RECIPIENT_ADDRESS_ERROR, KNOWN_RECIPIENT_ADDRESS_ERROR,
INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR, INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
CONFUSING_ENS_ERROR,
} from '../../send.constants'; } from '../../send.constants';
import { import {
isValidAddress, isValidAddress,
isEthNetwork, isEthNetwork,
checkExistingAddresses, checkExistingAddresses,
isValidDomainName,
} from '../../../../helpers/utils/util'; } from '../../../../helpers/utils/util';
export function getToErrorObject(to, hasHexData = false, network) { export function getToErrorObject(to, hasHexData = false, network) {
@ -36,6 +39,9 @@ export function getToWarningObject(to, tokens = [], sendToken = null) {
checkExistingAddresses(to, tokens)) checkExistingAddresses(to, tokens))
) { ) {
toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR; toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR;
} else if (isValidDomainName(to) && isConfusing(to)) {
toWarning = CONFUSING_ENS_ERROR;
} }
return { to: toWarning }; return { to: toWarning };
} }

@ -6,6 +6,7 @@ import {
REQUIRED_ERROR, REQUIRED_ERROR,
INVALID_RECIPIENT_ADDRESS_ERROR, INVALID_RECIPIENT_ADDRESS_ERROR,
KNOWN_RECIPIENT_ADDRESS_ERROR, KNOWN_RECIPIENT_ADDRESS_ERROR,
CONFUSING_ENS_ERROR,
} from '../../../send.constants'; } from '../../../send.constants';
const stubs = { const stubs = {
@ -93,5 +94,17 @@ describe('add-recipient utils', function () {
}, },
); );
}); });
it('should warn if name is a valid domain and confusable', function () {
assert.deepEqual(getToWarningObject('vitalik.eth'), {
to: CONFUSING_ENS_ERROR,
});
});
it('should not warn if name is a valid domain and not confusable', function () {
assert.deepEqual(getToWarningObject('vitalik.eth'), {
to: null,
});
});
}); });
}); });

@ -35,6 +35,7 @@ const INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR =
'invalidAddressRecipientNotEthNetwork'; 'invalidAddressRecipientNotEthNetwork';
const REQUIRED_ERROR = 'required'; const REQUIRED_ERROR = 'required';
const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient'; const KNOWN_RECIPIENT_ADDRESS_ERROR = 'knownAddressRecipient';
const CONFUSING_ENS_ERROR = 'confusingEnsDomain';
const SIMPLE_GAS_COST = '0x5208'; // Hex for 21000, cost of a simple send. const SIMPLE_GAS_COST = '0x5208'; // Hex for 21000, cost of a simple send.
const BASE_TOKEN_GAS_COST = '0x186a0'; // Hex for 100000, a base estimate for token transfers. const BASE_TOKEN_GAS_COST = '0x186a0'; // Hex for 100000, a base estimate for token transfers.
@ -53,6 +54,7 @@ export {
MIN_GAS_TOTAL, MIN_GAS_TOTAL,
NEGATIVE_ETH_ERROR, NEGATIVE_ETH_ERROR,
REQUIRED_ERROR, REQUIRED_ERROR,
CONFUSING_ENS_ERROR,
SIMPLE_GAS_COST, SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE, TOKEN_TRANSFER_FUNCTION_SIGNATURE,
BASE_TOKEN_GAS_COST, BASE_TOKEN_GAS_COST,

@ -24436,6 +24436,11 @@ unicode-canonical-property-names-ecmascript@^1.0.4:
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ== integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
unicode-confusables@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/unicode-confusables/-/unicode-confusables-0.1.1.tgz#17f14e8dc53ff81c12e92fd86e836ebdf14ea0c2"
integrity sha512-XTPBWmT88BDpXz9NycWk4KxDn+/AJmJYYaYBwuIH9119sopwk2E9GxU9azc+JNbhEsfiPul78DGocEihCp6MFQ==
unicode-match-property-ecmascript@^1.0.4: unicode-match-property-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c" resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"

Loading…
Cancel
Save