mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
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>
This commit is contained in:
parent
caa32d87fb
commit
b04120dd0f
@ -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"
|
||||||
|
39
ui/app/components/ui/confusable/confusable.component.js
Normal file
39
ui/app/components/ui/confusable/confusable.component.js
Normal file
@ -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;
|
1
ui/app/components/ui/confusable/index.js
Normal file
1
ui/app/components/ui/confusable/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from './confusable.component';
|
5
ui/app/components/ui/confusable/index.scss
Normal file
5
ui/app/components/ui/confusable/index.scss
Normal file
@ -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,34 +58,36 @@ 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,
|
||||||
<ReactTippy
|
{ className: wrapperClassName },
|
||||||
arrow={arrow}
|
<ReactTippy
|
||||||
className={containerClassName}
|
arrow={arrow}
|
||||||
disabled={disabled}
|
className={containerClassName}
|
||||||
hideOnClick={false}
|
disabled={disabled}
|
||||||
html={html}
|
hideOnClick={false}
|
||||||
interactive={interactive}
|
html={html}
|
||||||
onHidden={onHidden}
|
interactive={interactive}
|
||||||
position={position}
|
onHidden={onHidden}
|
||||||
size={size}
|
position={position}
|
||||||
offset={offset}
|
size={size}
|
||||||
style={style}
|
offset={offset}
|
||||||
title={title}
|
style={style}
|
||||||
trigger={trigger}
|
title={title}
|
||||||
theme={theme}
|
trigger={trigger}
|
||||||
tabIndex={tabIndex || 0}
|
theme={theme}
|
||||||
>
|
tabIndex={tabIndex || 0}
|
||||||
{children}
|
tag={tag}
|
||||||
</ReactTippy>
|
>
|
||||||
</div>
|
{children}
|
||||||
|
</ReactTippy>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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…
x
Reference in New Issue
Block a user