Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Network URL non-ascii -> punycode warning #12813

Merged
merged 14 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,37 @@ describe('NetworkVerificationInfo', () => {

expect(getByText('10')).toBeTruthy();
});

it('should not render Network URL warning banner when the custom rpc url has all ascii characters', () => {
(useSelector as jest.Mock).mockReturnValue(true);
const { getByText } = render(
<NetworkVerificationInfo
customNetworkInformation={mockNetworkInfo}
onReject={() => undefined}
onConfirm={() => undefined}
/>,
);

expect(() =>
getByText('Attackers sometimes mimic sites by making small changes to the site address. Make sure you\'re interacting with the intended Network URL before you continue. Punycode version: https://xn--ifura-dig.io/gnosis')
).toThrow('Unable to find an element with text');
});

describe('when the custom rpc url has non-ascii characters', () => {
it('should render Network URL warning banner and display punycode encoded version', () => {
(useSelector as jest.Mock).mockReturnValue(true);
const { getByText } = render(
<NetworkVerificationInfo
customNetworkInformation={{
...mockNetworkInfo,
rpcUrl: 'https://iոfura.io/gnosis',
}}
onReject={() => undefined}
onConfirm={() => undefined}
/>,
);

expect(getByText('Attackers sometimes mimic sites by making small changes to the site address. Make sure you\'re interacting with the intended Network URL before you continue. Punycode version: https://xn--ifura-dig.io/gnosis')).toBeTruthy();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { toggleUseSafeChainsListValidation } from '../../../util/networks/engine
import { NetworkApprovalBottomSheetSelectorsIDs } from '../../../../e2e/selectors/Network/NetworkApprovalBottomSheet.selectors';
import hideKeyFromUrl from '../../../util/hideKeyFromUrl';
import { convertHexToDecimal } from '@metamask/controller-utils';
import { isValidASCIIURL, toPunycodeURL } from '../../../util/url';

interface Alert {
alertError: string;
Expand Down Expand Up @@ -83,6 +84,8 @@ const NetworkVerificationInfo = ({
const showReviewDefaultRpcUrlChangesModal = () =>
setShowReviewDefaultRpcUrlChanges(!showReviewDefaultRpcUrlChanges);

const customRpcUrl = customNetworkInformation.rpcUrl;

const goToLearnMore = () => {
Linking.openURL(
'https://support.metamask.io/networks-and-sidechains/managing-networks/verifying-custom-network-information/',
Expand Down Expand Up @@ -237,7 +240,7 @@ const NetworkVerificationInfo = ({
</Text>
)}
<Text style={styles.textSection}>
{hideKeyFromUrl(customNetworkInformation.rpcUrl)}
{hideKeyFromUrl(customRpcUrl)}
</Text>

<Accordion
Expand Down Expand Up @@ -291,6 +294,25 @@ const NetworkVerificationInfo = ({
return null;
};

const renderBannerNetworkUrlNonAsciiDetected = () => {
if (isValidASCIIURL(customRpcUrl)) { return null; }
const punycodeUrl = toPunycodeURL(customRpcUrl);

return (
<View style={styles.alertBar}>
<Banner
severity={BannerAlertSeverity.Warning}
variant={BannerVariant.Alert}
description={
strings('networks.network_rpc_url_warning_punycode') +
'\n' +
punycodeUrl
}
/>
</View>
);
};

const renderCustomNetworkBanner = () => (
<View style={styles.alertBar}>
<Banner
Expand Down Expand Up @@ -332,7 +354,7 @@ const NetworkVerificationInfo = ({
{strings('networks.current_label')}
</Text>
<Text style={styles.textSection}>
{customNetworkInformation.rpcUrl}
{customRpcUrl}
</Text>
<Text variant={TextVariant.BodyMDBold}>
{strings('networks.new_label')}
Expand Down Expand Up @@ -442,6 +464,7 @@ const NetworkVerificationInfo = ({
/>
{renderAlerts()}
{renderBanner()}
{renderBannerNetworkUrlNonAsciiDetected()}
{isMultichainVersion1Enabled &&
isCustomNetwork &&
renderCustomNetworkBanner()}
Expand Down
31 changes: 31 additions & 0 deletions app/util/url/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import AppConstants from '../../core/AppConstants';
// eslint-disable-next-line import/no-nodejs-modules
import punycode from 'punycode';

export function isPortfolioUrl(url: string) {
try {
Expand All @@ -24,6 +26,35 @@ export function isBridgeUrl(url: string) {
}
}

export const isValidASCIIURL = (urlString?: string) => {
try {
return urlString?.includes(punycode.toASCII(new URL(urlString).host));
digiwand marked this conversation as resolved.
Show resolved Hide resolved
} catch (exp: unknown) {
console.error(exp);
digiwand marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
};
digiwand marked this conversation as resolved.
Show resolved Hide resolved

function removePathTrailingSlash(path: string) {
return path.endsWith('/') ? path.slice(0, -1) : path;
}

/**
* Note: We use the punycode library here because the url library in react native doesn't support punycode encoding.
* It is supported in node.js which allows tests to pass, but behavior in react-native does not match the
* behavior in the tests. This differs from the toPunycodeURL util method in metamask-extension.
*/
export const toPunycodeURL = (urlString: string) => {
try {
const url = new URL(urlString);
const punycodeHostname = punycode.toASCII(url.hostname);
const { protocol, port, search, hash } = url;
const pathname =
url.pathname === '/' && !urlString.endsWith('/') ? '' : url.pathname;

return `${protocol}//${punycodeHostname}${port}${pathname}${search}${hash}`;
digiwand marked this conversation as resolved.
Show resolved Hide resolved
} catch (err: unknown) {
console.error(`Failed to convert URL to Punycode: ${err}`);
return urlString;
}
};
34 changes: 33 additions & 1 deletion app/util/url/url.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isPortfolioUrl, isBridgeUrl } from './index';
import { isPortfolioUrl, isBridgeUrl, isValidASCIIURL, toPunycodeURL } from './index';
import AppConstants from '../../core/AppConstants';

describe('URL Check Functions', () => {
Expand Down Expand Up @@ -60,4 +60,36 @@ describe('URL Check Functions', () => {
expect(isBridgeUrl(url)).toBe(false);
});
});

describe('isValidASCIIURL', () => {
it('returns true for URL containing only ASCII characters', () => {
expect(isValidASCIIURL('https://www.google.com')).toEqual(true);
});

it('returns false for URL containing special character', () => {
expect(isValidASCIIURL('https://iոfura.io/gnosis')).toStrictEqual(false);
});
});

describe('toPunycodeURL', () => {
it('returns punycode version of URL', () => {
expect(toPunycodeURL('https://iոfura.io/gnosis')).toStrictEqual(
'https://xn--ifura-dig.io/gnosis',
);
expect(toPunycodeURL('https://iոfura.io')).toStrictEqual(
'https://xn--ifura-dig.io',
);
expect(toPunycodeURL('https://iոfura.io/')).toStrictEqual(
'https://xn--ifura-dig.io/',
);
expect(
toPunycodeURL('https://iոfura.io/gnosis:5050?test=iոfura&foo=bar'),
).toStrictEqual(
'https://xn--ifura-dig.io/gnosis:5050?test=i%D5%B8fura&foo=bar',
);
expect(toPunycodeURL('https://www.google.com')).toStrictEqual(
'https://www.google.com',
);
});
});
});
1 change: 1 addition & 0 deletions locales/languages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1814,6 +1814,7 @@
"network_chain_id": "Chain ID",
"network_rpc_url": "Network URL",
"network_rpc_url_label": "Network RPC URL",
"network_rpc_url_warning_punycode": "Attackers sometimes mimic sites by making small changes to the site address. Make sure you're interacting with the intended Network URL before you continue. Punycode version:",
"new_default_network_url": "New default network URL",
"current_label": "Current",
"new_label": "New",
Expand Down
Loading