Skip to content

Commit 001a031

Browse files
security(webview): restrict native JS channel callbacks to configured origin
Mirror the web iframe postMessage origin check on native InAppWebView JavaScript handlers so cross-origin pages loaded after navigation cannot invoke YAML actions wired to javascriptChannels. Co-authored-by: Sharjeel Yunus <sharjeelyunus@users.noreply.github.com>
1 parent f62fb1e commit 001a031

2 files changed

Lines changed: 91 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:meta/meta.dart';
2+
3+
/// Returns true when [messageOrigin] matches [allowedOrigin].
4+
///
5+
/// Used by native [InAppWebView] JavaScript channel handlers to mirror the
6+
/// origin check already applied to web iframe `postMessage` handling.
7+
@visibleForTesting
8+
bool isAllowedWebViewJavaScriptMessageOrigin({
9+
required String? messageOrigin,
10+
required String? allowedOrigin,
11+
}) {
12+
if (messageOrigin == null || allowedOrigin == null) {
13+
return false;
14+
}
15+
if (messageOrigin.isEmpty || allowedOrigin.isEmpty) {
16+
return false;
17+
}
18+
return messageOrigin == allowedOrigin;
19+
}
20+
21+
/// Derives the allowed postMessage / JS-bridge origin from the configured
22+
/// WebView [url]. Returns null for missing or non-http(s) URLs.
23+
@visibleForTesting
24+
String? webViewAllowedOriginFromUrl(String? url) {
25+
final uri = Uri.tryParse(url ?? '');
26+
if (uri == null || !uri.hasScheme || !uri.hasAuthority) {
27+
return null;
28+
}
29+
final scheme = uri.scheme.toLowerCase();
30+
if (scheme != 'http' && scheme != 'https') {
31+
return null;
32+
}
33+
final origin = uri.origin;
34+
return origin.isEmpty ? null : origin;
35+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import 'package:ensemble/widget/webview/webview_javascript_bridge_security.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('webViewAllowedOriginFromUrl', () {
6+
test('returns origin for https URLs', () {
7+
expect(
8+
webViewAllowedOriginFromUrl('https://app.example.com/path'),
9+
'https://app.example.com',
10+
);
11+
});
12+
13+
test('returns null for missing or non-http(s) schemes', () {
14+
expect(webViewAllowedOriginFromUrl(null), isNull);
15+
expect(webViewAllowedOriginFromUrl(''), isNull);
16+
expect(webViewAllowedOriginFromUrl('file:///etc/passwd'), isNull);
17+
expect(webViewAllowedOriginFromUrl('not-a-url'), isNull);
18+
});
19+
});
20+
21+
group('isAllowedWebViewJavaScriptMessageOrigin', () {
22+
test('allows matching origins', () {
23+
expect(
24+
isAllowedWebViewJavaScriptMessageOrigin(
25+
messageOrigin: 'https://trusted.example',
26+
allowedOrigin: 'https://trusted.example',
27+
),
28+
isTrue,
29+
);
30+
});
31+
32+
test('rejects cross-origin and empty values', () {
33+
expect(
34+
isAllowedWebViewJavaScriptMessageOrigin(
35+
messageOrigin: 'https://evil.example',
36+
allowedOrigin: 'https://trusted.example',
37+
),
38+
isFalse,
39+
);
40+
expect(
41+
isAllowedWebViewJavaScriptMessageOrigin(
42+
messageOrigin: 'https://evil.example',
43+
allowedOrigin: null,
44+
),
45+
isFalse,
46+
);
47+
expect(
48+
isAllowedWebViewJavaScriptMessageOrigin(
49+
messageOrigin: '',
50+
allowedOrigin: 'https://trusted.example',
51+
),
52+
isFalse,
53+
);
54+
});
55+
});
56+
}

0 commit comments

Comments
 (0)