URL: Mark mixed script usage as suspicious (#5693)
This commit is contained in:
parent
befbc6a4c9
commit
d0504015d4
@ -4,12 +4,8 @@ import { getActions } from '../../global';
|
||||
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import {
|
||||
DEBUG,
|
||||
} from '../../config';
|
||||
import convertPunycode from '../../lib/punycode';
|
||||
import { ensureProtocol, getUnicodeUrl, isMixedScriptUrl } from '../../util/browser/url';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { ensureProtocol } from '../../util/ensureProtocol';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
@ -39,7 +35,9 @@ const SafeLink = ({
|
||||
if (!url) return true;
|
||||
|
||||
e.preventDefault();
|
||||
openUrl({ url, shouldSkipModal: shouldSkipModal || isRegularLink });
|
||||
|
||||
const isTrustedLink = isRegularLink && !isMixedScriptUrl(url);
|
||||
openUrl({ url, shouldSkipModal: shouldSkipModal || isTrustedLink });
|
||||
|
||||
return false;
|
||||
});
|
||||
@ -69,33 +67,4 @@ const SafeLink = ({
|
||||
);
|
||||
};
|
||||
|
||||
function getUnicodeUrl(url?: string) {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const href = ensureProtocol(url);
|
||||
if (!href) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
const unicodeDomain = convertPunycode(parsedUrl.hostname);
|
||||
|
||||
try {
|
||||
return decodeURI(parsedUrl.toString()).replace(parsedUrl.hostname, unicodeDomain);
|
||||
} catch (err) { // URL contains invalid sequences, keep it as it is
|
||||
return parsedUrl.toString().replace(parsedUrl.hostname, unicodeDomain);
|
||||
}
|
||||
} catch (error) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('SafeLink.getDecodedUrl error ', url, error);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export default SafeLink;
|
||||
|
||||
@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import { ensureProtocol } from '../../util/ensureProtocol';
|
||||
import { ensureProtocol } from '../../util/browser/url';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
@ -20,6 +20,10 @@ const SafeLinkModal: FC<OwnProps> = ({ url }) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const handleOpen = useCallback(() => {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(ensureProtocol(url), '_blank', 'noopener');
|
||||
toggleSafeLinkModal({ url: undefined });
|
||||
}, [toggleSafeLinkModal, url]);
|
||||
@ -35,7 +39,7 @@ const SafeLinkModal: FC<OwnProps> = ({ url }) => {
|
||||
isOpen={Boolean(url)}
|
||||
onClose={handleDismiss}
|
||||
title={lang('OpenUrlTitle')}
|
||||
textParts={renderText(lang('OpenUrlAlert2', renderingUrl), ['links'])}
|
||||
textParts={renderText(lang('OpenUrlAlert2', renderingUrl))}
|
||||
confirmLabel={lang('OpenUrlTitle')}
|
||||
confirmHandler={handleOpen}
|
||||
/>
|
||||
|
||||
@ -7,9 +7,9 @@ import type { IAnchorPosition } from '../../../types';
|
||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||
|
||||
import { EDITABLE_INPUT_ID } from '../../../config';
|
||||
import { ensureProtocol } from '../../../util/browser/url';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import getKeyFromEvent from '../../../util/getKeyFromEvent';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
import { INPUT_CUSTOM_EMOJI_SELECTOR } from './helpers/customEmoji';
|
||||
|
||||
@ -16,8 +16,8 @@ import {
|
||||
selectThreadIdFromMessage,
|
||||
selectTopic,
|
||||
} from '../../../global/selectors';
|
||||
import { ensureProtocol } from '../../../util/browser/url';
|
||||
import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatStarsAsText } from '../../../util/localization/format';
|
||||
import { conjuctionWithNodes } from '../../../util/localization/utils';
|
||||
|
||||
@ -9,7 +9,7 @@ import type { TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import { ensureProtocol } from '../../../util/browser/url';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
@ -44,8 +44,8 @@ const UrlAuthModal: FC<OwnProps & StateProps> = ({
|
||||
acceptAction({
|
||||
isWriteAllowed: isWriteAccessChecked,
|
||||
});
|
||||
} else {
|
||||
window.open(ensureProtocol(currentAuth?.url), '_blank', 'noopener');
|
||||
} else if (currentAuth?.url) {
|
||||
window.open(ensureProtocol(currentAuth.url), '_blank', 'noopener');
|
||||
}
|
||||
closeUrlAuthModal();
|
||||
}, [
|
||||
|
||||
@ -37,9 +37,9 @@ import {
|
||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
} from '../../../config';
|
||||
import { ensureProtocol, isMixedScriptUrl } from '../../../util/browser/url';
|
||||
import { copyTextToClipboardFromPromise } from '../../../util/clipboard';
|
||||
import { isDeepLink } from '../../../util/deepLinkParser';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import {
|
||||
areSortedArraysIntersecting,
|
||||
@ -1866,6 +1866,8 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
|
||||
url, shouldSkipModal, ignoreDeepLinks, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const urlWithProtocol = ensureProtocol(url)!;
|
||||
const parsedUrl = new URL(urlWithProtocol);
|
||||
const isMixedScript = isMixedScriptUrl(urlWithProtocol);
|
||||
|
||||
if (!ignoreDeepLinks && isDeepLink(urlWithProtocol)) {
|
||||
actions.closeStoryViewer({ tabId });
|
||||
@ -1877,8 +1879,6 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
const { appConfig, config } = global;
|
||||
if (appConfig) {
|
||||
const parsedUrl = new URL(urlWithProtocol);
|
||||
|
||||
if (config?.autologinToken && appConfig.autologinDomains.includes(parsedUrl.hostname)) {
|
||||
parsedUrl.searchParams.set(AUTOLOGIN_TOKEN_KEY, config.autologinToken);
|
||||
window.open(parsedUrl.href, '_blank', 'noopener');
|
||||
@ -1896,9 +1896,9 @@ addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
|
||||
const shouldDisplayModal = !urlWithProtocol.match(RE_TELEGRAM_LINK) && !shouldSkipModal;
|
||||
|
||||
if (shouldDisplayModal) {
|
||||
actions.toggleSafeLinkModal({ url: urlWithProtocol, tabId });
|
||||
actions.toggleSafeLinkModal({ url: isMixedScript ? parsedUrl.toString() : urlWithProtocol, tabId });
|
||||
} else {
|
||||
window.open(urlWithProtocol, '_blank', 'noopener');
|
||||
window.open(parsedUrl, '_blank', 'noopener');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
67
src/util/browser/url.ts
Normal file
67
src/util/browser/url.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { DEBUG } from '../../config';
|
||||
import convertPunycode from '../../lib/punycode';
|
||||
|
||||
const PROTOCOL_WHITELIST = new Set(['http:', 'https:', 'tg:', 'ton:', 'mailto:', 'tel:']);
|
||||
const FALLBACK_PREFIX = 'https://';
|
||||
|
||||
export function ensureProtocol(url: string) {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (!PROTOCOL_WHITELIST.has(parsedUrl.protocol)) {
|
||||
return `${FALLBACK_PREFIX}${url}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (err) {
|
||||
return `${FALLBACK_PREFIX}${url}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUnicodeUrl(url: string) {
|
||||
const href = ensureProtocol(url);
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(href);
|
||||
const unicodeDomain = convertPunycode(parsedUrl.hostname);
|
||||
|
||||
try {
|
||||
return decodeURI(parsedUrl.toString()).replace(parsedUrl.hostname, unicodeDomain);
|
||||
} catch (err) { // URL contains invalid sequences, keep it as it is
|
||||
return parsedUrl.toString().replace(parsedUrl.hostname, unicodeDomain);
|
||||
}
|
||||
} catch (error) {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('SafeLink.getDecodedUrl error ', url, error);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isMixedScriptUrl(url: string): boolean {
|
||||
let domain;
|
||||
try {
|
||||
domain = convertPunycode(new URL(ensureProtocol(url)!).hostname);
|
||||
} catch (e) {
|
||||
return true; // If URL is invalid, treat it as mixed script
|
||||
}
|
||||
|
||||
let hasLatin = false;
|
||||
let hasNonLatin = false;
|
||||
|
||||
for (const char of Array.from(domain)) {
|
||||
if (!/\p{L}/u.test(char)) continue; // Ignore non-letter characters
|
||||
|
||||
if (/\p{Script=Latin}/u.test(char)) {
|
||||
hasLatin = true;
|
||||
} else {
|
||||
hasNonLatin = true;
|
||||
}
|
||||
|
||||
if (hasLatin && hasNonLatin) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -2,8 +2,8 @@ import type { ThreadId } from '../types';
|
||||
|
||||
import { RE_TG_LINK, RE_TME_LINK } from '../config';
|
||||
import { toChannelId } from '../global/helpers';
|
||||
import { ensureProtocol } from './browser/url';
|
||||
import { parseTimestampDuration } from './dates/timestamp';
|
||||
import { ensureProtocol } from './ensureProtocol';
|
||||
import { isUsernameValid } from './username';
|
||||
import { IS_BAD_URL_PARSER } from './windowEnvironment';
|
||||
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
const PROTOCOL_WHITELIST = new Set(['http:', 'https:', 'tg:', 'ton:', 'mailto:', 'tel:']);
|
||||
const FALLBACK_PREFIX = 'https://';
|
||||
|
||||
export function ensureProtocol(url?: string) {
|
||||
if (!url) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (!PROTOCOL_WHITELIST.has(parsedUrl.protocol)) {
|
||||
return `${FALLBACK_PREFIX}${url}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
} catch (err) {
|
||||
return `${FALLBACK_PREFIX}${url}`;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user