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": {
|
||||
"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": {
|
||||
"message": "Congratulations"
|
||||
},
|
||||
|
@ -177,6 +177,7 @@
|
||||
"single-call-balance-checker-abi": "^1.0.0",
|
||||
"swappable-obj-proxy": "^1.1.0",
|
||||
"textarea-caret": "^3.0.1",
|
||||
"unicode-confusables": "^0.1.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"web3": "^0.20.7",
|
||||
"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',
|
||||
wrapperClassName: undefined,
|
||||
theme: '',
|
||||
tag: 'div',
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
@ -36,6 +37,7 @@ export default class Tooltip extends PureComponent {
|
||||
style: PropTypes.object,
|
||||
theme: PropTypes.string,
|
||||
tabIndex: PropTypes.number,
|
||||
tag: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -56,34 +58,36 @@ export default class Tooltip extends PureComponent {
|
||||
style,
|
||||
theme,
|
||||
tabIndex,
|
||||
tag,
|
||||
} = this.props;
|
||||
|
||||
if (!title && !html) {
|
||||
return <div className={wrapperClassName}>{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<ReactTippy
|
||||
arrow={arrow}
|
||||
className={containerClassName}
|
||||
disabled={disabled}
|
||||
hideOnClick={false}
|
||||
html={html}
|
||||
interactive={interactive}
|
||||
onHidden={onHidden}
|
||||
position={position}
|
||||
size={size}
|
||||
offset={offset}
|
||||
style={style}
|
||||
title={title}
|
||||
trigger={trigger}
|
||||
theme={theme}
|
||||
tabIndex={tabIndex || 0}
|
||||
>
|
||||
{children}
|
||||
</ReactTippy>
|
||||
</div>
|
||||
return React.createElement(
|
||||
tag,
|
||||
{ className: wrapperClassName },
|
||||
<ReactTippy
|
||||
arrow={arrow}
|
||||
className={containerClassName}
|
||||
disabled={disabled}
|
||||
hideOnClick={false}
|
||||
html={html}
|
||||
interactive={interactive}
|
||||
onHidden={onHidden}
|
||||
position={position}
|
||||
size={size}
|
||||
offset={offset}
|
||||
style={style}
|
||||
title={title}
|
||||
trigger={trigger}
|
||||
theme={theme}
|
||||
tabIndex={tabIndex || 0}
|
||||
tag={tag}
|
||||
>
|
||||
{children}
|
||||
</ReactTippy>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
@import 'chip/chip';
|
||||
@import 'circle-icon/index';
|
||||
@import 'color-indicator/color-indicator';
|
||||
@import 'confusable/index';
|
||||
@import 'currency-display/index';
|
||||
@import 'currency-input/index';
|
||||
@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 { ellipsify } from '../../send.utils';
|
||||
import Button from '../../../../components/ui/button';
|
||||
import Confusable from '../../../../components/ui/confusable';
|
||||
|
||||
export default class AddRecipient extends Component {
|
||||
static propTypes = {
|
||||
@ -128,7 +129,7 @@ export default class AddRecipient extends Component {
|
||||
<Identicon address={address} diameter={28} />
|
||||
<div className="send__select-recipient-wrapper__group-item__content">
|
||||
<div className="send__select-recipient-wrapper__group-item__title">
|
||||
{name || ellipsify(address)}
|
||||
{name ? <Confusable input={name} /> : ellipsify(address)}
|
||||
</div>
|
||||
{name && (
|
||||
<div className="send__select-recipient-wrapper__group-item__subtitle">
|
||||
|
@ -1,16 +1,19 @@
|
||||
import ethUtil from 'ethereumjs-util';
|
||||
import contractMap from '@metamask/contract-metadata';
|
||||
import { isConfusing } from 'unicode-confusables';
|
||||
import {
|
||||
REQUIRED_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_ERROR,
|
||||
KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_NOT_ETH_NETWORK_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
} from '../../send.constants';
|
||||
|
||||
import {
|
||||
isValidAddress,
|
||||
isEthNetwork,
|
||||
checkExistingAddresses,
|
||||
isValidDomainName,
|
||||
} from '../../../../helpers/utils/util';
|
||||
|
||||
export function getToErrorObject(to, hasHexData = false, network) {
|
||||
@ -36,6 +39,9 @@ export function getToWarningObject(to, tokens = [], sendToken = null) {
|
||||
checkExistingAddresses(to, tokens))
|
||||
) {
|
||||
toWarning = KNOWN_RECIPIENT_ADDRESS_ERROR;
|
||||
} else if (isValidDomainName(to) && isConfusing(to)) {
|
||||
toWarning = CONFUSING_ENS_ERROR;
|
||||
}
|
||||
|
||||
return { to: toWarning };
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
REQUIRED_ERROR,
|
||||
INVALID_RECIPIENT_ADDRESS_ERROR,
|
||||
KNOWN_RECIPIENT_ADDRESS_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
} from '../../../send.constants';
|
||||
|
||||
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';
|
||||
const REQUIRED_ERROR = 'required';
|
||||
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 BASE_TOKEN_GAS_COST = '0x186a0'; // Hex for 100000, a base estimate for token transfers.
|
||||
@ -53,6 +54,7 @@ export {
|
||||
MIN_GAS_TOTAL,
|
||||
NEGATIVE_ETH_ERROR,
|
||||
REQUIRED_ERROR,
|
||||
CONFUSING_ENS_ERROR,
|
||||
SIMPLE_GAS_COST,
|
||||
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
|
||||
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"
|
||||
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:
|
||||
version "1.0.4"
|
||||
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