diff --git a/Telegram/Resources/bot_webview_shell_html/page.js b/Telegram/Resources/bot_webview_shell_html/page.js index a3b22c2bf3..9e8c5b937e 100644 --- a/Telegram/Resources/bot_webview_shell_html/page.js +++ b/Telegram/Resources/bot_webview_shell_html/page.js @@ -52,6 +52,8 @@ let iframe = null; let frameLoaded = false; let frameUrl = 'about:blank'; + let frameOrigin = ''; + let sameOrigin = false; let frameGeneration = 0; let reloadSupported = false; let reloadTimeout = null; @@ -85,7 +87,16 @@ && (!window.location.port || window.location.port === '443'); } - function invokeNative(source, eventType, eventData) { + function originFromUrl(url) { + try { + const origin = new URL(url, window.location.href).origin; + return origin && origin !== 'null' ? origin : ''; + } catch (e) { + return ''; + } + } + + function invokeNative(source, eventType, eventData, origin) { if (!window.external || typeof window.external.invoke !== 'function' || typeof shellToken !== 'string' @@ -100,7 +111,7 @@ type: nativeMessageType, source: source, token: shellToken, - origin: window.location.origin, + origin: origin || window.location.origin, eventType: eventType, eventData: normalizeEventData(eventData) })); @@ -110,8 +121,8 @@ invokeNative('shell', eventType, eventData); } - function invokeWebApp(eventType, eventData) { - invokeNative('webapp', eventType, eventData); + function invokeWebApp(eventType, eventData, origin) { + invokeNative('webapp', eventType, eventData, origin); } function sendToFrame(eventType, eventData, generation) { @@ -120,10 +131,14 @@ || generation !== frameGeneration) { return; } + if (sameOrigin && !frameOrigin) { + return; + } + const targetOrigin = sameOrigin ? frameOrigin : '*'; iframe.contentWindow.postMessage(JSON.stringify({ eventType: eventType, eventData: eventData || {} - }), '*'); + }), targetOrigin); } function postToFrame(eventType, eventData) { @@ -838,6 +853,9 @@ || event.source !== iframe.contentWindow) { return; } + if (sameOrigin && (!frameOrigin || event.origin !== frameOrigin)) { + return; + } const message = parseFrameMessage(event.data); if (!message || typeof message.eventType !== 'string') { return; @@ -853,7 +871,7 @@ frameLoaded = false; return; } - invokeWebApp(message.eventType, message.eventData); + invokeWebApp(message.eventType, message.eventData, event.origin); }); menuBackdrop.addEventListener('mousedown', closeMenu); @@ -981,7 +999,9 @@ shellState.bottomText = ''; title.textContent = (data && data.title) || ''; document.title = (data && data.title) || 'Telegram'; + sameOrigin = !!(data && data.sameOrigin); frameUrl = (data && data.url) || 'about:blank'; + frameOrigin = sameOrigin ? originFromUrl(frameUrl) : ''; createIframe(frameUrl); renderButtons(); renderMenu(); diff --git a/Telegram/SourceFiles/api/api_chat_invite.cpp b/Telegram/SourceFiles/api/api_chat_invite.cpp index f2bd7ff180..e1a855fa96 100644 --- a/Telegram/SourceFiles/api/api_chat_invite.cpp +++ b/Telegram/SourceFiles/api/api_chat_invite.cpp @@ -660,8 +660,7 @@ void ProcessChatInviteJoinResult( .maySkipConfirmation = false, }, .source = InlineBots::WebViewSourceJoinChat{ - .url = qs(data.vurl()), - .queryId = uint64(data.vquery_id().v), + .result = InlineBots::ParseWebViewResult(data.vwebview()), }, }); }); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index bfdc8f8af0..af57d90dcc 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -820,6 +820,17 @@ void BotAction::handleKeyPress(not_null e) { } // namespace +WebViewResultData ParseWebViewResult(const MTPWebViewResult &result) { + const auto &data = result.data(); + return { + .url = qs(data.vurl()), + .queryId = data.vquery_id().value_or_empty(), + .fullscreen = data.is_fullscreen(), + .fullsize = data.is_fullsize(), + .sameOrigin = data.is_same_origin(), + }; +} + base::weak_ptr WebViewInstance::PendingActivation; MenuBotIcon::MenuBotIcon( @@ -954,7 +965,7 @@ void WebViewInstance::requestFullBot() { ) | rpl::on_next([=] { if (_botFullWaitingArgs.has_value()) { auto args = *base::take(_botFullWaitingArgs); - if (args.url.isEmpty()) { + if (args.result.url.isEmpty()) { showGame(); } else { show(std::move(args)); @@ -1015,8 +1026,7 @@ void WebViewInstance::resolve() { }, [&](WebViewSourceJoinChat data) { confirmOpen([=] { show({ - .url = data.url, - .queryId = data.queryId, + .result = data.result, }); }, true); }); @@ -1214,11 +1224,8 @@ void WebViewInstance::requestButton() { ? action.options.sendAs->input() : MTP_inputPeerEmpty()) )).done([=](const MTPWebViewResult &result) { - const auto &data = result.data(); show({ - .url = qs(data.vurl()), - .queryId = data.vquery_id().value_or_empty(), - .fullscreen = data.is_fullscreen(), + .result = ParseWebViewResult(result), }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); @@ -1248,10 +1255,8 @@ void WebViewInstance::requestSimple() { MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_string("tdesktop") )).done([=](const MTPWebViewResult &result) { - const auto &data = result.data(); show({ - .url = qs(data.vurl()), - .fullscreen = data.is_fullscreen(), + .result = ParseWebViewResult(result), }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); @@ -1278,10 +1283,8 @@ void WebViewInstance::requestMain() { MTP_dataJSON(MTP_bytes(botThemeParams().json)), MTP_string("tdesktop") )).done([=](const MTPWebViewResult &result) { - const auto &data = result.data(); show({ - .url = qs(data.vurl()), - .fullscreen = data.is_fullscreen(), + .result = ParseWebViewResult(result), }); }).fail([=](const MTP::Error &error) { _parentShow->showToast(error.type()); @@ -1309,11 +1312,9 @@ void WebViewInstance::requestApp(bool allowWrite) { MTP_string("tdesktop") )).done([=](const MTPWebViewResult &result) { _requestId = 0; - const auto &data = result.data(); show({ - .url = qs(data.vurl()), + .result = ParseWebViewResult(result), .title = title, - .fullscreen = data.is_fullscreen(), }); }).fail([=](const MTP::Error &error) { _requestId = 0; @@ -1437,20 +1438,21 @@ void WebViewInstance::show(ShowArgs &&args) { || (attached != end(bots) && (attached->inAttachMenu || attached->inMainMenu)); const auto downloads = &_session->attachWebView().downloads(); - _panelUrl = args.url; + _panelUrl = args.result.url; _panel = Ui::BotWebView::Show({ - .url = args.url, + .url = args.result.url, .storageId = _session->local().resolveStorageIdBots(), .title = std::move(title), .titleBadge = std::move(titleBadge), .bottom = rpl::single('@' + _bot->username()), .delegate = static_cast(this), .menuButtons = buttons, - .fullscreen = args.fullscreen, + .fullscreen = args.result.fullscreen, + .sameOrigin = args.result.sameOrigin, .allowClipboardRead = allowClipboardRead, .downloadsProgress = downloads->progress(_bot), }); - started(args.queryId); + started(args.result.queryId); if (const auto strong = PendingActivation.get()) { if (strong == this) { diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h index 53cb0a6c7e..156ed6978c 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.h @@ -173,9 +173,23 @@ struct WebViewSourceAgeVerification { } }; -struct WebViewSourceJoinChat { +struct WebViewResultData { QString url; uint64 queryId = 0; + bool fullscreen = false; + bool fullsize = false; + bool sameOrigin = false; + + friend inline bool operator==( + const WebViewResultData &, + const WebViewResultData &) = default; +}; + +[[nodiscard]] WebViewResultData ParseWebViewResult( + const MTPWebViewResult &result); + +struct WebViewSourceJoinChat { + WebViewResultData result; friend inline bool operator==( const WebViewSourceJoinChat &, @@ -269,10 +283,8 @@ private: bool forceConfirmation); struct ShowArgs { - QString url; + WebViewResultData result; QString title; - uint64 queryId = 0; - bool fullscreen = false; }; void show(ShowArgs &&args); void showGame(); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp index 7a33397629..c712b70d5e 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.cpp @@ -79,6 +79,7 @@ enum class NativeMessageSource { struct NativeMessage { NativeMessageSource source = NativeMessageSource::LegacyWebApp; + QString origin; QString command; QJsonObject arguments; }; @@ -115,6 +116,48 @@ struct NativeMessage { && url.fragment().isEmpty(); } +[[nodiscard]] int EffectivePort(const QUrl &url) { + const auto explicitPort = url.port(-1); + if (explicitPort >= 0) { + return explicitPort; + } + const auto scheme = url.scheme().toLower(); + if (scheme == u"http"_q) { + return 80; + } else if (scheme == u"https"_q) { + return 443; + } + return -1; +} + +[[nodiscard]] QString OriginFromUrl(const QString &url) { + const auto parsed = QUrl(url); + if (!parsed.isValid()) { + return {}; + } + const auto scheme = parsed.scheme().toLower(); + auto host = parsed.host().toLower(); + const auto port = EffectivePort(parsed); + if (scheme.isEmpty() || host.isEmpty() || port < 0) { + return {}; + } + if (host.contains(':') && !host.startsWith('[')) { + host = u"["_q + host + u"]"_q; + } + return u"%1://%2:%3"_q.arg(scheme, host, QString::number(port)); +} + +[[nodiscard]] bool OriginsMatch(const QString &a, const QString &b) { + if (a.isEmpty() || b.isEmpty()) { + return false; + } + const auto normalizedA = OriginFromUrl(a); + const auto normalizedB = OriginFromUrl(b); + return !normalizedA.isEmpty() + && !normalizedB.isEmpty() + && normalizedA == normalizedB; +} + [[nodiscard]] RectPart ParsePosition(const QString &position) { if (position == u"left"_q) { return RectPart::Left; @@ -203,6 +246,7 @@ void LogNativeMessageRejected( [[nodiscard]] std::optional ParseNativeMessage( const QByteArray &bytes, + const QString &sourceUrl, bool externalShell, const QString &shellToken) { const auto byteCount = quint64(bytes.size()); @@ -248,8 +292,8 @@ void LogNativeMessageRejected( || token.toString() != shellToken) { return reject(u"bad external token"_q); } + const auto origin = object.value(u"origin"_q); if (source == NativeMessageSource::ExternalShell) { - const auto origin = object.value(u"origin"_q); if (!origin.isString() || !IsExternalShellOrigin(origin.toString())) { return reject(u"bad shell origin"_q); @@ -273,6 +317,9 @@ void LogNativeMessageRejected( } return NativeMessage{ .source = source, + .origin = (source == NativeMessageSource::ExternalWebApp) + ? origin.toString() + : QString(), .command = command, .arguments = arguments, }; @@ -306,6 +353,7 @@ void LogNativeMessageRejected( } return NativeMessage{ .source = NativeMessageSource::LegacyWebApp, + .origin = OriginFromUrl(sourceUrl), .command = command, .arguments = arguments, }; @@ -1137,7 +1185,8 @@ Panel::Panel(Args &&args) ? std::make_unique() : nullptr) , _fullscreen(args.fullscreen) -, _allowClipboardRead(args.allowClipboardRead) { +, _allowClipboardRead(args.allowClipboardRead) +, _sameOrigin(args.sameOrigin) { if (_externalShell) { _widget->setAttribute(Qt::WA_DontShowOnScreen); _externalLayer->boxAdded( @@ -1501,6 +1550,9 @@ void Panel::hideWebviewProgress() { bool Panel::showWebview(Args &&args, const Webview::ThemeParams ¶ms) { _bottomText = std::move(args.bottom); _externalUrl = args.url; + _sameOrigin = args.sameOrigin; + _initialOrigin = OriginFromUrl(args.url); + _currentOrigin = _initialOrigin; if (_externalShell && !_webview) { resetExternalShellIdentity(); } @@ -1661,6 +1713,8 @@ void Panel::sendExternalShellBootstrap() { const auto params = _delegate->botThemeParams(); sendExternalShellMethod("bootstrap", { { u"url"_q, _externalUrl }, + { u"sameOrigin"_q, _sameOrigin }, + { u"initialOrigin"_q, _initialOrigin }, { u"title"_q, _externalTitle }, { u"metrics"_q, LinuxShell::Metrics() }, { u"colors"_q, LinuxShell::ColorPayload(externalShellColors(params)) }, @@ -2189,23 +2243,33 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { }, _webview->lifetime); } - raw->setMessageHandler([=](std::string text) { - if (text.size() > size_t(kMaxNativeMessageBytes)) { + raw->setMessageHandler([=](Webview::Message message) { + if (message.text.size() > size_t(kMaxNativeMessageBytes)) { LogNativeMessageRejected( u"payload too large"_q, - quint64(text.size())); + quint64(message.text.size())); return; } const auto bytes = QByteArray::fromRawData( - text.data(), - int(text.size())); + message.text.data(), + int(message.text.size())); const auto parsed = ParseNativeMessage( bytes, + QString::fromStdString(message.sourceUrl), _externalShell, _externalShellToken); if (!parsed) { return; } + if (_sameOrigin + && parsed->source != NativeMessageSource::ExternalShell + && !OriginsMatch(parsed->origin, _initialOrigin)) { + LogNativeMessageRejected( + u"bad webapp origin"_q, + quint64(message.text.size()), + parsed->command); + return; + } const auto &command = parsed->command; const auto &arguments = parsed->arguments; if (parsed->source == NativeMessageSource::ExternalShell) { @@ -2372,6 +2436,7 @@ bool Panel::createWebview(const Webview::ThemeParams ¶ms) { } else if (newWindow) { return true; } + _currentOrigin = OriginFromUrl(uri); showWebviewProgress(); return true; }); @@ -3644,6 +3709,9 @@ void Panel::postEvent(const QString &event, EventData data) { } return; } + if (_sameOrigin && !OriginsMatch(_currentOrigin, _initialOrigin)) { + return; + } auto written = v::is(data) ? v::get(data).toUtf8() : QJsonDocument( diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h index 6dd9239046..9a216947dd 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h +++ b/Telegram/SourceFiles/ui/chat/attach/attach_bot_webview.h @@ -149,6 +149,7 @@ struct Args { not_null delegate; MenuButtons menuButtons; bool fullscreen = false; + bool sameOrigin = false; bool allowClipboardRead = false; rpl::producer downloadsProgress; }; @@ -329,6 +330,8 @@ private: bool _externalShellBootstrapped = false; bool _externalWindowCloseRequested = false; QString _externalShellToken; + QString _initialOrigin; + QString _currentOrigin; uint64 _externalShellGeneration = 0; bool _externalBackVisible = false; ExternalShellColorState _externalShellColorState; @@ -364,6 +367,7 @@ private: bool _hiddenForPayment : 1 = false; bool _closeWithConfirmationScheduled : 1 = false; bool _allowClipboardRead : 1 = false; + bool _sameOrigin : 1 = false; bool _inBlockingRequest : 1 = false; bool _headerColorReceived : 1 = false; bool _bodyColorReceived : 1 = false; diff --git a/Telegram/lib_webview b/Telegram/lib_webview index 0a4fecacc1..15748218dd 160000 --- a/Telegram/lib_webview +++ b/Telegram/lib_webview @@ -1 +1 @@ -Subproject commit 0a4fecacc156d8aefbc18dea4557ad8e87a08581 +Subproject commit 15748218dd435f56c9d296f0de44c51556181173