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 {