From 840615222f26946435e69017818fd200d7b2c7c0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 25 Jun 2026 09:08:17 +0000 Subject: [PATCH] fix(security): prevent Chart.js config injection into eval/HTML ChartJs interpolated string config directly into browser eval() on web and into inline HTML/JS on native. Attacker-controlled config from API responses or user input could break out of the Chart constructor call and execute arbitrary JavaScript. Validate string configs as JSON and emit via jsonEncode. Preserve trusted Map-based configs that may include author-defined JS callbacks. Reject unsafe chart ids embedded in HTML/JS contexts. Adds regression tests. Co-authored-by: Sharjeel Yunus --- modules/ensemble/lib/util/chart_utils.dart | 41 +++++++++++- .../visualization/chart_js/chart_js.dart | 15 ++++- .../chart_js/native/chart_js_state.dart | 3 +- .../chart_js/web/chart_js_state.dart | 7 +- .../ensemble/test/chart_js_security_test.dart | 67 +++++++++++++++++++ 5 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 modules/ensemble/test/chart_js_security_test.dart diff --git a/modules/ensemble/lib/util/chart_utils.dart b/modules/ensemble/lib/util/chart_utils.dart index 92bcca32c..4c89b478a 100644 --- a/modules/ensemble/lib/util/chart_utils.dart +++ b/modules/ensemble/lib/util/chart_utils.dart @@ -1,4 +1,38 @@ +import 'dart:convert'; + +import 'package:meta/meta.dart'; + class ChartUtils { + static final RegExp _safeChartId = RegExp(r'^[a-zA-Z0-9_]+$'); + + /// Returns true when [chartId] is safe to embed in HTML attributes and JS. + @visibleForTesting + static bool isSafeChartId(String chartId) => _safeChartId.hasMatch(chartId); + + /// Builds a JavaScript expression that evaluates to a Chart.js config object. + /// + /// Map-based configs may include author-defined JavaScript callbacks and are + /// emitted as trusted object literals. String configs are validated as JSON + /// and emitted via [jsonEncode] to prevent eval injection. + @visibleForTesting + static String buildSafeChartConfigExpression( + String config, { + required bool configFromMap, + }) { + if (configFromMap) { + return config; + } + if (config.isEmpty) { + return '{}'; + } + try { + final parsed = jsonDecode(config); + return jsonEncode(parsed); + } catch (_) { + return '{}'; + } + } + static String getClickEventScript(String chartId, {bool isWeb = false}) { // For web, we use the chart variable name pattern final chartReference = isWeb ? 'myChart$chartId' : 'window.chart'; @@ -46,7 +80,10 @@ class ChartUtils { '''; } - static String getBaseHtml(String chartId, String config) { + static String getBaseHtml(String chartId, String config, + {bool configFromMap = false}) { + final configExpr = + buildSafeChartConfigExpression(config, configFromMap: configFromMap); return ''' @@ -63,7 +100,7 @@ class ChartUtils {