Skip to content

Commit c88dd07

Browse files
security(tabapay): validate postMessage origin in WebView listener
The TabaPay WebView installed an unrestricted window message listener that forwarded every postMessage payload to the Flutter messageHandler channel. A cross-origin frame or page could spoof payment success callbacks and trigger YAML-defined onSuccess actions with attacker-controlled token data. Restrict forwarding to messages whose origin matches the configured iframe URL and fail closed when the URL is not a valid http(s) origin. Add regression tests for the generated listener script. Co-authored-by: Sharjeel Yunus <sharjeelyunus@users.noreply.github.com>
1 parent 628d9b1 commit c88dd07

3 files changed

Lines changed: 70 additions & 2 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import 'dart:convert';
2+
3+
/// Builds JavaScript that forwards `postMessage` payloads to the WebView
4+
/// [messageHandler] channel only when `event.origin` matches the origin of
5+
/// [pageUrl].
6+
///
7+
/// Returns `null` when [pageUrl] is not a valid absolute `http`/`https` URI so
8+
/// callers can fail closed instead of installing an unrestricted listener.
9+
String? buildTabaPayPostMessageListenerScript(String pageUrl) {
10+
final uri = Uri.tryParse(pageUrl);
11+
if (uri == null || !uri.hasScheme || !uri.hasAuthority) {
12+
return null;
13+
}
14+
final scheme = uri.scheme.toLowerCase();
15+
if (scheme != 'http' && scheme != 'https') {
16+
return null;
17+
}
18+
final origin = uri.origin;
19+
if (origin.isEmpty) {
20+
return null;
21+
}
22+
final originLiteral = jsonEncode(origin);
23+
return '''
24+
window.addEventListener("message", function(event) {
25+
if (event.origin !== $originLiteral) {
26+
return;
27+
}
28+
messageHandler.postMessage(event.data);
29+
});
30+
''';
31+
}

modules/ensemble/lib/widget/fintech/tabapayconnect.dart

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:webview_flutter/webview_flutter.dart';
1010
import '../../framework/action.dart';
1111
import '../../screen_controller.dart';
1212
import '../../util/utils.dart';
13+
import 'tabapay_post_message.dart';
1314

1415
class TabaPayConnectController extends WidgetController {
1516
String uri =
@@ -86,8 +87,11 @@ class TabaPayConnectState extends EWidgetState<TabaPayConnect> {
8687
onMessageReceived: _handleTabaPayMessage)
8788
..setNavigationDelegate(
8889
NavigationDelegate(onPageFinished: (String url) {
89-
_webViewController?.runJavaScript(
90-
'window.addEventListener("message", (event) => messageHandler.postMessage(event.data))');
90+
final listenerScript =
91+
buildTabaPayPostMessageListenerScript(widget.controller.uri);
92+
if (listenerScript != null) {
93+
_webViewController?.runJavaScript(listenerScript);
94+
}
9195
}));
9296
_webViewController?.loadRequest(Uri.parse(widget.controller.uri));
9397
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import 'package:ensemble/widget/fintech/tabapay_post_message.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
4+
void main() {
5+
group('buildTabaPayPostMessageListenerScript', () {
6+
test('includes origin check for valid https iframe URL', () {
7+
final script = buildTabaPayPostMessageListenerScript(
8+
'https://iframe.tabapay.com/frame',
9+
);
10+
expect(script, isNotNull);
11+
expect(script, contains('"https://iframe.tabapay.com"'));
12+
expect(script, contains('event.origin !=='));
13+
expect(script, contains('messageHandler.postMessage(event.data)'));
14+
});
15+
16+
test('returns null for invalid or non-http(s) URLs', () {
17+
expect(buildTabaPayPostMessageListenerScript(''), isNull);
18+
expect(buildTabaPayPostMessageListenerScript('not-a-url'), isNull);
19+
expect(
20+
buildTabaPayPostMessageListenerScript('javascript:alert(1)'),
21+
isNull,
22+
);
23+
expect(buildTabaPayPostMessageListenerScript('file:///etc/passwd'), isNull);
24+
});
25+
26+
test('JSON-encodes origin literal for safe embedding in JavaScript', () {
27+
final script = buildTabaPayPostMessageListenerScript(
28+
'https://pay.example.com/frame',
29+
);
30+
expect(script, contains('"https://pay.example.com"'));
31+
});
32+
});
33+
}

0 commit comments

Comments
 (0)