diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index 9f7e292b8..b0425051c 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -52,6 +52,7 @@ export interface GramJsAppConfig extends LimitsConfig { autologin_domains: string[]; autologin_token: string; url_auth_domains: string[]; + web_app_allowed_protocols?: string[]; whitelisted_domains: string[]; premium_purchase_blocked: boolean; giveaway_gifts_purchase_available: boolean; @@ -184,6 +185,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp autologinDomains: appConfig.autologin_domains || [], urlAuthDomains: appConfig.url_auth_domains || [], whitelistedDomains: appConfig.whitelisted_domains || [], + webAppAllowedProtocols: appConfig.web_app_allowed_protocols, maxUniqueReactions: appConfig.reactions_uniq_max, premiumBotUsername: appConfig.premium_bot_username, premiumInvoiceSlug: appConfig.premium_invoice_slug, diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index fc04ca803..9e87f7222 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -257,6 +257,7 @@ export interface ApiAppConfig { autologinDomains: string[]; urlAuthDomains: string[]; whitelistedDomains: string[]; + webAppAllowedProtocols: string[]; premiumInvoiceSlug?: string; premiumBotUsername: string; isPremiumPurchaseBlocked: boolean; diff --git a/src/components/modals/webApp/hooks/useWebAppFrame.ts b/src/components/modals/webApp/hooks/useWebAppFrame.ts index 668494de4..026c5c4b0 100644 --- a/src/components/modals/webApp/hooks/useWebAppFrame.ts +++ b/src/components/modals/webApp/hooks/useWebAppFrame.ts @@ -7,6 +7,7 @@ import type { WebApp, WebAppInboundEvent, WebAppOutboundEvent } from '../../../. import { VERIFY_AGE_MIN_DEFAULT } from '../../../../config'; import { getWebAppKey } from '../../../../global/helpers'; import { isMessageFromIframe } from '../../../../util/browser/iframe'; +import { isValidProtocol } from '../../../../util/browser/url'; import { extractCurrentThemeParams } from '../../../../util/themeStyle'; import { REM } from '../../../common/helpers/mediaDimensions'; @@ -241,8 +242,11 @@ const useWebAppFrame = ( } if (eventType === 'web_app_open_link') { - const linkUrl = eventData.url; - window.open(linkUrl, '_blank', 'noreferrer'); + if (!isValidProtocol(eventData.url, getGlobal().appConfig.webAppAllowedProtocols)) { + return; + } + + window.open(eventData.url, '_blank', 'noopener,noreferrer'); } if (eventType === 'web_app_biometry_get_info') { diff --git a/src/global/cache.ts b/src/global/cache.ts index 2ca918942..6f9ff916a 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -369,6 +369,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cached.appConfig = initialState.appConfig; } + if (cached.appConfig.webAppAllowedProtocols === undefined) { + cached.appConfig.webAppAllowedProtocols = initialState.appConfig.webAppAllowedProtocols; + } + if (untypedCached.sharedState?.settings?.shouldWarnAboutSvg) { cached.sharedState.settings.shouldWarnAboutFiles = true; untypedCached.sharedState.settings.shouldWarnAboutSvg = undefined; diff --git a/src/limits.ts b/src/limits.ts index 4978bde17..e8e64cb18 100644 --- a/src/limits.ts +++ b/src/limits.ts @@ -150,6 +150,10 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = { 'z.t.me', 'a.t.me', ], + webAppAllowedProtocols: [ + 'http', + 'https', + ], whitelistedDomains: [ 'telegram.dog', 'telegram.me', diff --git a/src/util/browser/url.ts b/src/util/browser/url.ts index 159ff8b29..a37345a56 100644 --- a/src/util/browser/url.ts +++ b/src/util/browser/url.ts @@ -4,6 +4,10 @@ import convertPunycode from '../../lib/punycode'; const PROTOCOL_WHITELIST = new Set(['http:', 'https:', 'tg:', 'ton:', 'mailto:', 'tel:']); const FALLBACK_PREFIX = 'https://'; +function normalizeProtocol(protocol: string) { + return protocol.replace(/:$/, '').trim().toLowerCase(); +} + export function ensureProtocol(url: string) { try { const parsedUrl = new URL(url); @@ -65,3 +69,17 @@ export function isMixedScriptUrl(url: string): boolean { return false; } + +export function isValidProtocol(url: string, allowedProtocols: string[]) { + if (typeof url !== 'string') { + return false; + } + + try { + const parsedUrl = new URL(url); + + return allowedProtocols.includes(normalizeProtocol(parsedUrl.protocol)); + } catch (err) { + return false; + } +}