diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.js b/src/Core/src/Handlers/HybridWebView/HybridWebView.js index 7e374fa85754..23a12054f1fc 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.js +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.js @@ -23,8 +23,10 @@ // Determine the mechanism to receive messages from the host application. if (window.chrome && window.chrome.webview && window.chrome.webview.addEventListener) { // Windows WebView2 + // The .NET side URL-encodes messages (see MauiHybridWebView.SendRawMessage) so embedded + // NUL characters survive WebView2's null-terminated string marshalling. Decode here. window.chrome.webview.addEventListener('message', (arg) => { - dispatchHybridWebViewMessage(arg.data); + dispatchHybridWebViewMessage(decodeURIComponent(arg.data)); }); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { @@ -56,7 +58,10 @@ // Determine the function to use to send messages to the host application. if (window.chrome && window.chrome.webview) { // Windows WebView2 - sendMessageFunction = msg => window.chrome.webview.postMessage(msg); + // URL-encode so embedded NUL characters survive WebView2's null-terminated string + // marshalling (TryGetWebMessageAsString returns an LPWSTR); the .NET side decodes it + // in HybridWebViewHandler.OnWebMessageReceived. + sendMessageFunction = msg => window.chrome.webview.postMessage(encodeURIComponent(msg)); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts index 81f57bb8068b..46e64f11f058 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebView.ts +++ b/src/Core/src/Handlers/HybridWebView/HybridWebView.ts @@ -73,8 +73,10 @@ interface DotNetInvokeResult { // Determine the mechanism to receive messages from the host application. if (window.chrome && window.chrome.webview && window.chrome.webview.addEventListener) { // Windows WebView2 + // The .NET side URL-encodes messages (see MauiHybridWebView.SendRawMessage) so embedded + // NUL characters survive WebView2's null-terminated string marshalling. Decode here. window.chrome.webview.addEventListener('message', (arg: any) => { - dispatchHybridWebViewMessage(arg.data); + dispatchHybridWebViewMessage(decodeURIComponent(arg.data)); }); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView @@ -105,7 +107,10 @@ interface DotNetInvokeResult { // Determine the function to use to send messages to the host application. if (window.chrome && window.chrome.webview) { // Windows WebView2 - sendMessageFunction = msg => window.chrome.webview.postMessage(msg); + // URL-encode so embedded NUL characters survive WebView2's null-terminated string + // marshalling (TryGetWebMessageAsString returns an LPWSTR); the .NET side decodes it + // in HybridWebViewHandler.OnWebMessageReceived. + sendMessageFunction = msg => window.chrome.webview.postMessage(encodeURIComponent(msg)); } else if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.webwindowinterop) { // iOS and MacCatalyst WKWebView sendMessageFunction = msg => window.webkit.messageHandlers.webwindowinterop.postMessage(msg); diff --git a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs index f9cef27e49cd..d40a58668b87 100644 --- a/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs +++ b/src/Core/src/Handlers/HybridWebView/HybridWebViewHandler.Windows.cs @@ -98,7 +98,10 @@ public static void MapSendRawMessage(IHybridWebViewHandler handler, IHybridWebVi private void OnWebMessageReceived(WebView2 sender, CoreWebView2WebMessageReceivedEventArgs args) { - MessageReceived(args.TryGetWebMessageAsString()); + // The JS transport URL-encodes messages so embedded NUL characters survive WebView2's + // null-terminated string marshalling (TryGetWebMessageAsString returns an LPWSTR). Decode + // the payload before dispatching it. + MessageReceived(Uri.UnescapeDataString(args.TryGetWebMessageAsString())); } internal static void MapFlowDirection(IHybridWebViewHandler handler, IHybridWebView hybridWebView) diff --git a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs index 76f5610c89de..654dd54ebecb 100644 --- a/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs +++ b/src/Core/src/Platform/Android/MauiHybridWebViewClient.cs @@ -69,14 +69,36 @@ public override void OnPageFinished(AWebView? view, string? url) if (view is not null && request is not null && !string.IsNullOrEmpty(url)) { - // 1. Check if the app wants to modify or override the request + // 1. Framework-internal bridge requests must be handled by the framework and + // never exposed to app-level WebResourceRequested interception. See + // IsFrameworkInternalRequest: each reserved endpoint is bound to BOTH its + // well-known path and (for the message/invoke channels) the protocol marker + // header, so the header alone is never a trust boundary. Before JS -> .NET + // messages were routed over HTTP they were invisible to app interception, and + // this preserves that invariant. + if (IsFrameworkInternalRequest(url, request)) + { + // A framework-internal request must be handled by the framework. If it + // cannot be resolved, fail fast with a 400 rather than forwarding it to the + // app handler. + return GetResponse(url, request, logger) + ?? new WebResourceResponse(null, "UTF-8", 400, "Bad Request", null, new MemoryStream()); + } + + // 2. Check if the app wants to modify or override the request. This path is + // intentionally left unwrapped: if a user WebResourceRequested handler throws + // for a legitimate app-origin request, the exception propagates exactly as it + // did before bridge traffic was routed over HTTP. Only the framework's own + // .NET dispatch (Handler.MessageReceived in GetResponse) is exception-isolated, + // because it runs under a JNI stack where an unhandled throw crashes the + // native WebView thread. var response = WebRequestInterceptingWebView.TryInterceptResponseStream(Handler, view, request, url, logger); if (response is not null) { return response; } - // 2. Check if the request is for a local resource + // 3. Check if the request is for a local resource response = GetResponse(url, request, logger); if (response is not null) { @@ -90,6 +112,52 @@ public override void OnPageFinished(AWebView? view, string? url) return base.ShouldInterceptRequest(view, request); } + // Resolves the app-origin-relative path for a request URL. Returns false when the URL is + // not under the HybridWebView app origin; returns true otherwise, with relativePath set to + // the resolved path (which may itself be null if the path could not be resolved). Shared by + // IsFrameworkInternalRequest and GetResponse to keep the URI parsing in one place. + static bool TryGetAppRelativePath(string fullUrl, out string? relativePath) + { + relativePath = null; + + var requestUri = WebUtils.RemovePossibleQueryString(fullUrl); + if (new Uri(requestUri) is not Uri uri || !HybridWebViewHandler.AppOriginUri.IsBaseOf(uri)) + { + return false; + } + + relativePath = WebUtils.ResolveRelativePath(HybridWebViewHandler.AppOriginUri, uri); + return true; + } + + // Returns true when the request targets a reserved HybridWebView bridge endpoint and must + // therefore be handled by the framework instead of being exposed to app-level + // WebResourceRequested interception. Each endpoint is bound to its well-known path: + // - the bridge bootstrap script is a plain