From 021d800d75a871406b17169ee8569d2f379eb3cd Mon Sep 17 00:00:00 2001 From: Om Patel Date: Fri, 24 Oct 2025 10:51:26 -0400 Subject: [PATCH 1/2] adding validation to params dict Other than selected properties and HTML, all strings in params are validated. --- .../dynamicreporting/core/common_utils.py | 70 +++++++++++++++++++ .../core/serverless/template.py | 3 + .../core/utils/report_objects.py | 5 ++ 3 files changed, 78 insertions(+) diff --git a/src/ansys/dynamicreporting/core/common_utils.py b/src/ansys/dynamicreporting/core/common_utils.py index f588b5b11..2012b1ff1 100644 --- a/src/ansys/dynamicreporting/core/common_utils.py +++ b/src/ansys/dynamicreporting/core/common_utils.py @@ -3,6 +3,8 @@ import platform import re +import bleach + from . import DEFAULT_ANSYS_VERSION as CURRENT_VERSION from .constants import JSON_NECESSARY_KEYS, JSON_TEMPLATE_KEYS, REPORT_TYPES from .exceptions import InvalidAnsysPath @@ -189,3 +191,71 @@ def populate_template(id_str, attr, parent_template, create_template_func, logge template.set_filter(filter_str=attr["item_filter"] if "item_filter" in attr else "") return template + + +PROPERTIES_EXEMPT = { + "link_text", + "userdef_name", + "filter_x_title", + "filter_y_title", + "labels_column", + "labels_row", + "title", + "line_marker_text", + "plot_title", + "xtitle", + "ytitle", + "ztitle", + "nan_display", + "table_title", + "image_title", + "slider_title", + "TOCName", +} + + +def validate_html_dictionary(data): + if not isinstance(data, dict): + raise TypeError("Input data must be a dictionary.") + + for key, value in data.items(): + # Do not validate HTML key + if key == "HTML": + continue + + # Recursive case for nested dictionaries + if isinstance(value, dict): + # Specific checks for properties key + if key == "properties": + subdict = {k: v for k, v in value.items() if k not in PROPERTIES_EXEMPT} + validate_html_dictionary(subdict) + else: + validate_html_dictionary(value) + + # Check for lists + elif isinstance(value, list): + validate_html_list(value, key) + + # Main check for strings + elif isinstance(value, str): + cleaned_string = bleach.clean(value, strip=True) + if cleaned_string != value: + raise ValueError(f"{key} contains HTML content.") + + # Ignore other types + else: + continue + + +def validate_html_list(value_list, key): + for item in value_list: + if isinstance(item, str): + cleaned_string = bleach.clean(item, strip=True) + if cleaned_string != item: + raise ValueError(f"{key} contains HTML content.") + elif isinstance(item, dict): + validate_html_dictionary(item) + elif isinstance(item, list): + validate_html_list(item, key) + else: + continue diff --git a/src/ansys/dynamicreporting/core/serverless/template.py b/src/ansys/dynamicreporting/core/serverless/template.py index c8cb1bb98..53fffb2fe 100644 --- a/src/ansys/dynamicreporting/core/serverless/template.py +++ b/src/ansys/dynamicreporting/core/serverless/template.py @@ -8,6 +8,7 @@ from django.template.loader import render_to_string from django.utils import timezone +from ..common_utils import validate_html_dictionary from ..constants import JSON_ATTR_KEYS from ..exceptions import ADRException, TemplateDoesNotExist, TemplateReorderOutOfBounds from .base import BaseModel, StrEnum @@ -251,6 +252,8 @@ def set_params(self, new_params: dict) -> None: new_params = {} if not isinstance(new_params, dict): raise TypeError("input must be a dictionary") + if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1": + validate_html_dictionary(new_params) self.params = json.dumps(new_params) def add_params(self, new_params: dict): diff --git a/src/ansys/dynamicreporting/core/utils/report_objects.py b/src/ansys/dynamicreporting/core/utils/report_objects.py index b1d9f83be..064232a22 100755 --- a/src/ansys/dynamicreporting/core/utils/report_objects.py +++ b/src/ansys/dynamicreporting/core/utils/report_objects.py @@ -22,6 +22,7 @@ import pytz from . import extremely_ugly_hacks, report_utils +from ..common_utils import validate_html_dictionary from ..exceptions import TemplateDoesNotExist, TemplateReorderOutOfBounds from .encoders import PayloaddataEncoder @@ -413,6 +414,8 @@ def set_params(self, d: dict = None): d = {} if type(d) is not dict: raise ValueError("Error: input must be a dictionary") + if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1": + validate_html_dictionary(d) self.params = json.dumps(d) return @@ -1527,6 +1530,8 @@ def set_params(self, d: dict = None): d = {} if type(d) is not dict: raise ValueError("Error: input must be a dictionary") + if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1": + validate_html_dictionary(d) self.params = json.dumps(d) return From 4258d73c6e7f06ca5f402723a2db6b4943565c8d Mon Sep 17 00:00:00 2001 From: Om Patel Date: Mon, 27 Oct 2025 11:01:30 -0400 Subject: [PATCH 2/2] added test cases --- .../dynamicreporting/core/common_utils.py | 3 - tests/test_report_objects.py | 57 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/ansys/dynamicreporting/core/common_utils.py b/src/ansys/dynamicreporting/core/common_utils.py index 2012b1ff1..3dc8f1197 100644 --- a/src/ansys/dynamicreporting/core/common_utils.py +++ b/src/ansys/dynamicreporting/core/common_utils.py @@ -215,9 +215,6 @@ def populate_template(id_str, attr, parent_template, create_template_func, logge def validate_html_dictionary(data): - if not isinstance(data, dict): - raise TypeError("Input data must be a dictionary.") - for key, value in data.items(): # Do not validate HTML key if key == "HTML": diff --git a/tests/test_report_objects.py b/tests/test_report_objects.py index 78564ad9c..be7e1f4aa 100755 --- a/tests/test_report_objects.py +++ b/tests/test_report_objects.py @@ -1,5 +1,6 @@ import datetime import json +import os import uuid import pytest @@ -1964,6 +1965,62 @@ def test_unit_template() -> None: assert succ and succ_two and succ_three and succ_four +@pytest.mark.ado_test +def test_template_validation() -> None: + os.environ["ADR_VALIDATION_BETAFLAG_ANSYS"] = "1" + a = ro.Template() + try: + a.set_params( + { + "reduce_params": { + "reduce_type": "row", + "operations": [ + "test 1", + ["test 2", 1], + {"source_rows": "'Phase*'", "output_rows": "Maximum"}, + ], + }, + "properties": {"plot": "line", "plot_title": "Reduced Table"}, + "HTML": "
Test
", + } + ) + except ValueError as e: + succ_one = "contains HTML content" in str(e) + try: + a.set_params( + { + "reduce_params": { + "reduce_type": "row", + "operations": [ + ["test 2", 1], + {"source_rows": "'Phase*'", "output_rows": "Maximum"}, + "test 1", + ], + }, + "properties": {"plot": "line", "plot_title": "Reduced Table"}, + "HTML": "
Test
", + } + ) + except ValueError as e: + succ_two = "contains HTML content" in str(e) + a.set_params( + { + "reduce_params": { + "reduce_type": "row", + "operations": [ + "test 1", + ["test 2", 1], + {"source_rows": "'Phase*'", "output_rows": "Maximum"}, + ], + }, + "HTML": "
Test
", + "properties": {"plot": "line", "plot_title": "Reduced Table"}, + } + ) + del os.environ["ADR_VALIDATION_BETAFLAG_ANSYS"] + assert succ_one and succ_two + + @pytest.mark.ado_test def test_unit_base() -> None: a = ro.BaseRESTObject()