Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion modules/ensemble/lib/widget/webview/native/webviewstate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:ensemble/framework/event.dart';
import 'package:ensemble/framework/widget/widget.dart';
import 'package:ensemble/screen_controller.dart';
import 'package:ensemble/widget/webview/webview.dart';
import 'package:ensemble/widget/webview/webview_javascript_bridge_security.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
Expand Down Expand Up @@ -205,7 +206,16 @@ class WebViewState extends EWidgetState<EnsembleWebView> with CookieMethods {
for (var channel in widget.controller.javascriptChannels) {
controller.addJavaScriptHandler(
handlerName: channel.name,
callback: (args) {
callback: (args) async {
final currentUrl = await controller.getUrl();
final allowedOrigin =
webViewAllowedOriginFromUrl(widget.controller.url);
if (!isAllowedWebViewJavaScriptMessageOrigin(
messageOrigin: currentUrl?.origin,
allowedOrigin: allowedOrigin,
)) {
return null;
}
if (channel.onMessageReceived != null) {
String message = '';
if (args.isNotEmpty) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:meta/meta.dart';

/// Returns true when [messageOrigin] matches [allowedOrigin].
///
/// Used by native [InAppWebView] JavaScript channel handlers to mirror the
/// origin check already applied to web iframe `postMessage` handling.
@visibleForTesting
bool isAllowedWebViewJavaScriptMessageOrigin({
required String? messageOrigin,
required String? allowedOrigin,
}) {
if (messageOrigin == null || allowedOrigin == null) {
return false;
}
if (messageOrigin.isEmpty || allowedOrigin.isEmpty) {
return false;
}
return messageOrigin == allowedOrigin;
}

/// Derives the allowed postMessage / JS-bridge origin from the configured
/// WebView [url]. Returns null for missing or non-http(s) URLs.
@visibleForTesting
String? webViewAllowedOriginFromUrl(String? url) {
final uri = Uri.tryParse(url ?? '');
if (uri == null || !uri.hasScheme || !uri.hasAuthority) {
return null;
}
final scheme = uri.scheme.toLowerCase();
if (scheme != 'http' && scheme != 'https') {
return null;
}
final origin = uri.origin;
return origin.isEmpty ? null : origin;
}
56 changes: 56 additions & 0 deletions modules/ensemble/test/webview_javascript_bridge_security_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import 'package:ensemble/widget/webview/webview_javascript_bridge_security.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('webViewAllowedOriginFromUrl', () {
test('returns origin for https URLs', () {
expect(
webViewAllowedOriginFromUrl('https://app.example.com/path'),
'https://app.example.com',
);
});

test('returns null for missing or non-http(s) schemes', () {
expect(webViewAllowedOriginFromUrl(null), isNull);
expect(webViewAllowedOriginFromUrl(''), isNull);
expect(webViewAllowedOriginFromUrl('file:///etc/passwd'), isNull);
expect(webViewAllowedOriginFromUrl('not-a-url'), isNull);
});
});

group('isAllowedWebViewJavaScriptMessageOrigin', () {
test('allows matching origins', () {
expect(
isAllowedWebViewJavaScriptMessageOrigin(
messageOrigin: 'https://trusted.example',
allowedOrigin: 'https://trusted.example',
),
isTrue,
);
});

test('rejects cross-origin and empty values', () {
expect(
isAllowedWebViewJavaScriptMessageOrigin(
messageOrigin: 'https://evil.example',
allowedOrigin: 'https://trusted.example',
),
isFalse,
);
expect(
isAllowedWebViewJavaScriptMessageOrigin(
messageOrigin: 'https://evil.example',
allowedOrigin: null,
),
isFalse,
);
expect(
isAllowedWebViewJavaScriptMessageOrigin(
messageOrigin: '',
allowedOrigin: 'https://trusted.example',
),
isFalse,
);
});
});
}
Loading