From d2518d9a4c0a72a4c2eb9fe72477027d670d40e7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 09:11:58 +0000 Subject: [PATCH] security(finicity): JSON-encode user-controlled values in WebView script Finicity Connect web renderer interpolated uri, overlay, and position directly into generated JavaScript. Attacker-influenced bindings could break out of string literals and execute arbitrary code in the WebView. Co-authored-by: Sharjeel Yunus --- .../fintech/finicity_connect_script.dart | 62 ++++++++++++++++++ .../web/finicityconnectstate.dart | 64 +++---------------- .../test/finicity_connect_script_test.dart | 48 ++++++++++++++ 3 files changed, 120 insertions(+), 54 deletions(-) create mode 100644 modules/ensemble/lib/widget/fintech/finicity_connect_script.dart create mode 100644 modules/ensemble/test/finicity_connect_script_test.dart diff --git a/modules/ensemble/lib/widget/fintech/finicity_connect_script.dart b/modules/ensemble/lib/widget/fintech/finicity_connect_script.dart new file mode 100644 index 000000000..eef77c70e --- /dev/null +++ b/modules/ensemble/lib/widget/fintech/finicity_connect_script.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +/// Builds the JavaScript snippet that launches Finicity Connect inside a +/// [JsWidget] WebView. +/// +/// All externally influenced string values are embedded via [jsonEncode] so +/// they cannot break out of the generated script. +String buildFinicityConnectInstantiateScript({ + required String connectUri, + required String widgetId, + required int left, + required int top, + required String position, + String? overlay, +}) { + final overlayLine = + overlay != null ? 'overlay: ${jsonEncode(overlay)},\n ' : ''; + final uriLiteral = jsonEncode(connectUri); + final widgetIdLiteral = jsonEncode(widgetId); + final positionLiteral = jsonEncode(position); + + return ''' + window.finicityConnect.launch($uriLiteral, { + $overlayLine success: (event) => { + console.log('Yay! User went through Connect', event); + event = {type:'success',data:event}; + handleMessage($widgetIdLiteral,JSON.stringify(event)); + }, + cancel: (event) => { + console.log('The user cancelled the iframe', event); + event = {type:'cancel',data:event}; + handleMessage($widgetIdLiteral,JSON.stringify(event)); + }, + error: (error) => { + console.error('Some runtime error was generated during insideConnect ', error); + event = {type:'error',data:error}; + handleMessage($widgetIdLiteral,JSON.stringify(error)); + }, + loaded: (event) => { + console.log('This gets called only once after the iframe has finished loading '); + event = {type:'loaded',data:event}; + handleMessage($widgetIdLiteral,JSON.stringify(event)); + }, + route: (event) => { + console.log('This is called as the user navigates through Connect ', event); + event = {type:'route',data:event}; + handleMessage($widgetIdLiteral,JSON.stringify(event)); + }, + user: (event) => { + console.log('This is called as the user interacts with Connect ', event); + event = {type:'user',data:event}; + handleMessage($widgetIdLiteral,JSON.stringify(event)); + } + }); + const finIFrame = document.getElementById("finicityConnectIframe"); + if ( finIFrame ) { + finIFrame.style.left = '${left}px'; + finIFrame.style.top = '${top}px'; + finIFrame.style.position = $positionLiteral; + } + '''; +} diff --git a/modules/ensemble/lib/widget/fintech/finicityconnect/web/finicityconnectstate.dart b/modules/ensemble/lib/widget/fintech/finicityconnect/web/finicityconnectstate.dart index c0739690e..9b9a99bbc 100644 --- a/modules/ensemble/lib/widget/fintech/finicityconnect/web/finicityconnectstate.dart +++ b/modules/ensemble/lib/widget/fintech/finicityconnect/web/finicityconnectstate.dart @@ -1,65 +1,15 @@ import 'package:ensemble/widget/fintech/finicityconnect/finicityconnect.dart'; +import 'package:ensemble/widget/fintech/finicity_connect_script.dart'; import 'package:flutter/material.dart'; import 'package:js_widget/js_widget.dart'; import 'dart:convert'; class FinicityConnectState extends FinicityConnectStateBase { - String getScriptToInstantiate( - String c, String width, String height, String overlay) { - return ''' - window.finicityConnect.launch("$c", { - $overlay - success: (event) => { - console.log('Yay! User went through Connect', event); - event = {type:'success',data:event}; - handleMessage('${widget.controller.id}',JSON.stringify(event)); - }, - cancel: (event) => { - console.log('The user cancelled the iframe', event); - event = {type:'cancel',data:event}; - handleMessage('${widget.controller.id}',JSON.stringify(event)); - }, - error: (error) => { - console.error('Some runtime error was generated during insideConnect ', error); - event = {type:'error',data:error}; - handleMessage('${widget.controller.id}',JSON.stringify(error)); - }, - loaded: (event) => { - console.log('This gets called only once after the iframe has finished loading '); - event = {type:'loaded',data:event}; - handleMessage('${widget.controller.id}',JSON.stringify(event)); - }, - route: (event) => { - console.log('This is called as the user navigates through Connect ', event); - event = {type:'route',data:event}; - handleMessage('${widget.controller.id}',JSON.stringify(event)); - }, - user: (event) => { - console.log('This is called as the user interacts with Connect ', event); - event = {type:'user',data:event}; - handleMessage('${widget.controller.id}',JSON.stringify(event)); - } - }); - const finIFrame = document.getElementById("finicityConnectIframe"); - if ( finIFrame ) { - finIFrame.style.left = '${widget.controller.left}px'; - finIFrame.style.top = '${widget.controller.top}px'; - finIFrame.style.position = '${widget.controller.position}'; - //finIFrame.style.width = '$width'; - //finIFrame.style.height = '$height'; - } - '''; - } - @override Widget buildWidget(BuildContext context) { if (widget.controller.uri == '') { return const Text(""); } - String overlay = ''; - if (widget.controller.overlay != null) { - overlay = 'overlay: ${widget.controller.overlay!},'; - } String width = '100%'; if (widget.controller.width != 0) { width = '${widget.controller.width}px'; @@ -75,9 +25,15 @@ class FinicityConnectState extends FinicityConnectStateBase { print('message inside finicity and the message is $msg!'); executeAction(json.decode(msg)); }, - scriptToInstantiate: (String c) { - return getScriptToInstantiate(c, width, height, overlay); - //return 'if (typeof ${widget.controller.chartVar} !== "undefined") ${widget.controller.chartVar}.destroy();${widget.controller.chartVar} = new Chart(document.getElementById("${widget.controller.chartId}"), $c);${widget.controller.chartVar}.update();'; + scriptToInstantiate: (String connectUri) { + return buildFinicityConnectInstantiateScript( + connectUri: connectUri, + widgetId: widget.controller.id!, + left: widget.controller.left, + top: widget.controller.top, + position: widget.controller.position, + overlay: widget.controller.overlay?.toString(), + ); }, size: Size(widget.controller.width.toDouble(), widget.controller.height.toDouble()), diff --git a/modules/ensemble/test/finicity_connect_script_test.dart b/modules/ensemble/test/finicity_connect_script_test.dart new file mode 100644 index 000000000..5eb7d5117 --- /dev/null +++ b/modules/ensemble/test/finicity_connect_script_test.dart @@ -0,0 +1,48 @@ +import 'package:ensemble/widget/fintech/finicity_connect_script.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('buildFinicityConnectInstantiateScript', () { + test('JSON-encodes connect URI for safe embedding', () { + final script = buildFinicityConnectInstantiateScript( + connectUri: 'https://connect2.finicity.com?token=abc', + widgetId: 'finicityConnect', + left: 0, + top: 10, + position: 'absolute', + ); + expect( + script, + contains('window.finicityConnect.launch("https://connect2.finicity.com?token=abc"'), + ); + }); + + test('neutralizes JavaScript breakout in connect URI', () { + const malicious = '"); alert(1); //'; + final script = buildFinicityConnectInstantiateScript( + connectUri: malicious, + widgetId: 'finicityConnect', + left: 0, + top: 0, + position: 'absolute', + ); + expect(script, contains('window.finicityConnect.launch(')); + expect(script, isNot(contains('launch(""); alert(1);'))); + expect(script, contains(r'\"); alert(1); //')); + }); + + test('JSON-encodes overlay and position values', () { + final script = buildFinicityConnectInstantiateScript( + connectUri: 'https://example.com', + widgetId: 'widget-1', + left: 4, + top: 8, + position: "absolute'; alert(1); '", + overlay: "rgba(0,0,0,0.5)", + ); + expect(script, contains('overlay: "rgba(0,0,0,0.5)",')); + expect(script, contains(r"position = \"absolute'; alert(1); '\"")); + expect(script, isNot(contains("position = 'absolute'; alert(1);"))); + }); + }); +}