From 74afd7803afe473ee69583efd37231ab91249a9f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 09:10:59 +0000 Subject: [PATCH] security(ensemble): sanitize global script handler call arguments Coerce untrusted handler inputs into safe JavaScript call arguments before embedding them in generated function snippets. Valid JSON from callers is passed through; raw strings are JSON-encoded to prevent breakout from deeplink, notification, and BLE handler paths. Co-authored-by: Sharjeel Yunus --- .../global_script_handler_security.dart | 16 +++++++++ modules/ensemble/lib/screen_controller.dart | 4 ++- .../global_script_handler_security_test.dart | 33 +++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 modules/ensemble/lib/framework/global_script_handler_security.dart create mode 100644 modules/ensemble/test/global_script_handler_security_test.dart diff --git a/modules/ensemble/lib/framework/global_script_handler_security.dart b/modules/ensemble/lib/framework/global_script_handler_security.dart new file mode 100644 index 000000000..8b235d3b9 --- /dev/null +++ b/modules/ensemble/lib/framework/global_script_handler_security.dart @@ -0,0 +1,16 @@ +import 'dart:convert'; + +/// Coerces [inputs] into a safe JavaScript call argument for embedding in a +/// generated `functionName()` snippet. +/// +/// Valid JSON literals (including quoted strings from [jsonEncode]) are passed +/// through unchanged. Any other value is wrapped with [jsonEncode] so quotes and +/// parentheses cannot break out of the argument position. +String toSafeJavaScriptCallArgument(String inputs) { + try { + jsonDecode(inputs); + return inputs; + } on FormatException { + return jsonEncode(inputs); + } +} diff --git a/modules/ensemble/lib/screen_controller.dart b/modules/ensemble/lib/screen_controller.dart index cd7f02fd1..fa139b266 100644 --- a/modules/ensemble/lib/screen_controller.dart +++ b/modules/ensemble/lib/screen_controller.dart @@ -14,6 +14,7 @@ import 'package:ensemble/framework/data_context.dart'; import 'package:ensemble/framework/devmode.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:ensemble/framework/event.dart'; +import 'package:ensemble/framework/global_script_handler_security.dart'; import 'package:ensemble/framework/scope.dart'; import 'package:ensemble/framework/stub/camera_manager.dart'; import 'package:ensemble/framework/stub/face_camera_manager.dart'; @@ -111,7 +112,8 @@ class ScreenController { final library = data[0]; final function = data[1]; - final codeBlock = "$function($inputs)"; + final safeInputs = toSafeJavaScriptCallArgument(inputs); + final codeBlock = "$function($safeInputs)"; payload = executeGlobalFunction( Utils.globalAppKey.currentContext!, library, codeBlock); return payload; diff --git a/modules/ensemble/test/global_script_handler_security_test.dart b/modules/ensemble/test/global_script_handler_security_test.dart new file mode 100644 index 000000000..2772c5ebf --- /dev/null +++ b/modules/ensemble/test/global_script_handler_security_test.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:ensemble/framework/global_script_handler_security.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('toSafeJavaScriptCallArgument', () { + test('passes through JSON object literals', () { + const payload = '{"data":{"link":"https://example.com"}}'; + expect(toSafeJavaScriptCallArgument(payload), payload); + }); + + test('passes through JSON-encoded string arguments', () { + const payload = '"hello world"'; + expect(toSafeJavaScriptCallArgument(payload), payload); + }); + + test('wraps raw attacker-controlled strings as JSON string literals', () { + const malicious = '"); alert(1); //'; + expect( + toSafeJavaScriptCallArgument(malicious), + jsonEncode(malicious), + ); + }); + + test('neutralizes BLE-style breakout attempts', () { + const malicious = 'x"); evil();//'; + final safe = toSafeJavaScriptCallArgument(malicious); + expect(safe, jsonEncode(malicious)); + expect(safe, isNot(contains('"); evil();//'))); + }); + }); +}