diff --git a/packages/extension-ui/src/partials/AccountSelection.tsx b/packages/extension-ui/src/partials/AccountSelection.tsx index 7a72131c..8b3dc35a 100644 --- a/packages/extension-ui/src/partials/AccountSelection.tsx +++ b/packages/extension-ui/src/partials/AccountSelection.tsx @@ -8,6 +8,7 @@ import { AccountContext, Checkbox, Warning } from '../components/index.js'; import { useTranslation } from '../hooks/index.js'; import AccountsTree from '../Popup/Accounts/AccountsTree.js'; import { styled } from '../styled.js'; +import { sanitizeOrigin, validateOrigin } from '../util/sanitizeOrigin.js'; interface Props { className?: string; @@ -20,7 +21,10 @@ interface Props { function AccountSelection ({ className, origin, showHidden = false, url, withWarning = true }: Props): React.ReactElement { const { t } = useTranslation(); const { accounts, hierarchy, selectedAccounts = [], setSelectedAccounts } = useContext(AccountContext); + const [sanitizedOrigin, setSanitizedOrigin] = useState(''); + const [isSuspiciousOrigin, setIsSuspiciousOrigin] = useState(false); const [isIndeterminate, setIsIndeterminate] = useState(false); + const allVisibleAccounts = useMemo(() => accounts.filter(({ isHidden }) => !isHidden), [accounts]); const noAccountSelected = useMemo(() => selectedAccounts.length === 0, [selectedAccounts.length]); const allDisplayedAddresses = useMemo( @@ -34,6 +38,14 @@ function AccountSelection ({ className, origin, showHidden = false, url, withWar , [allDisplayedAddresses.length, selectedAccounts.length] ); + useEffect(() => { + const cleaned = sanitizeOrigin(origin); + const isValid = validateOrigin(origin); + + setSanitizedOrigin(cleaned); + setIsSuspiciousOrigin(!isValid || cleaned !== origin); + }, [origin]); + useEffect(() => { const nextIndeterminateState = !noAccountSelected && !areAllAccountsSelected; @@ -55,7 +67,7 @@ function AccountSelection ({ className, origin, showHidden = false, url, withWar
{withWarning && ( - An application, self-identifying as {origin} is requesting access from{' '} + An application, self-identifying as {sanitizedOrigin} is requesting access from{' '} )} + {isSuspiciousOrigin && ( + + {t('It looks like this request is coming from an suspicious origin. Please verify the source carefully.')} + + )} ` width: 100% } } + + .warningError { + padding-block: 0.3rem; + margin-top: 1rem; + border: 1px solid var(--errorBorderColor); + background-color: #AF111170; + + [data-theme="light"] & { + color: black; + } + } `; diff --git a/packages/extension-ui/src/util/sanitizeOrigin.ts b/packages/extension-ui/src/util/sanitizeOrigin.ts new file mode 100644 index 00000000..56e8d62f --- /dev/null +++ b/packages/extension-ui/src/util/sanitizeOrigin.ts @@ -0,0 +1,58 @@ +// Copyright 2019-2025 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +const sanitizeHtml = (input: string, options?: { + allowLineBreaks?: boolean; + maxLength?: number; +}): string => { + if (!input || typeof input !== 'string') { + return ''; + } + + let sanitized = input; + + // Remove all HTML tags + sanitized = sanitized.replace(/<[^>]*>/g, ''); + + // Handle line breaks if specified + if (options?.allowLineBreaks) { + // Convert
to actual line breaks before sanitization + sanitized = input + .replace(//gi, '\n') + .replace(/<[^>]*>/g, ''); // Remove other HTML tags + } + + // Normalize whitespace + sanitized = sanitized + .trim() + .replace(/\s+/g, ' '); + + // Apply length limit if specified + if (options?.maxLength && sanitized.length > options.maxLength) { + sanitized = sanitized.substring(0, options.maxLength) + '...'; + } + + return sanitized; +}; + +export const sanitizeOrigin = (origin: string): string => { + return sanitizeHtml(origin, { + allowLineBreaks: false, + maxLength: 50 + }); +}; + +// Validate that the origin looks legitimate +export const validateOrigin = (origin: string): boolean => { + const sanitized = sanitizeOrigin(origin); + + // Check for suspicious patterns + const suspiciousPatterns = [ + /https?:\/\//i, // URLs in origin name + /[<>]/, // Remaining HTML characters + /^\s*$/, // Empty or whitespace only + /.{50,}/ // Extremely long names + ]; + + return !suspiciousPatterns.some((pattern) => pattern.test(sanitized)); +}; diff --git a/packages/extension/public/locales/en/translation.json b/packages/extension/public/locales/en/translation.json index e7049a2a..6c059a47 100644 --- a/packages/extension/public/locales/en/translation.json +++ b/packages/extension/public/locales/en/translation.json @@ -181,5 +181,6 @@ "This network is not available, please report an issue to update the known chains": "", "Ledger App": "", "Don't ask again": "", - "Previous": "" + "Previous": "", + "It looks like this request is coming from an suspicious origin. Please verify the source carefully.": "" }