Skip to content

Commit 0aa5d92

Browse files
authored
Add input validation for params dictionary (#431)
**Changes Made** - Added a new helper function `validate_html_dictionary()` to recursively validate HTML content within dictionaries. - Added `validate_html_list()` to handle lists that may contain other nested lists or dictionaries. - Created a `PROPERTIES_EXEMPT` set to exclude certain properties from validation where HTML is expected or unnecessary to sanitize. - Wrapped validation logic under an environment variable (`ADR_VALIDATION_BETAFLAG_ANSYS`) to allow toggling validation on/off for easier testing and to avoid affecting users. User Story: https://tfs.ansys.com:8443/tfs/ANSYS_Development/Portfolio/_workitems/edit/1249514
1 parent 5ecad29 commit 0aa5d92

File tree

4 files changed

+132
-0
lines changed

4 files changed

+132
-0
lines changed

src/ansys/dynamicreporting/core/common_utils.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import platform
44
import re
55

6+
import bleach
7+
68
from . import DEFAULT_ANSYS_VERSION as CURRENT_VERSION
79
from .constants import JSON_NECESSARY_KEYS, JSON_TEMPLATE_KEYS, REPORT_TYPES
810
from .exceptions import InvalidAnsysPath
@@ -189,3 +191,68 @@ def populate_template(id_str, attr, parent_template, create_template_func, logge
189191
template.set_filter(filter_str=attr["item_filter"] if "item_filter" in attr else "")
190192

191193
return template
194+
195+
196+
PROPERTIES_EXEMPT = {
197+
"link_text",
198+
"userdef_name",
199+
"filter_x_title",
200+
"filter_y_title",
201+
"labels_column",
202+
"labels_row",
203+
"title",
204+
"line_marker_text",
205+
"plot_title",
206+
"xtitle",
207+
"ytitle",
208+
"ztitle",
209+
"nan_display",
210+
"table_title",
211+
"image_title",
212+
"slider_title",
213+
"TOCName",
214+
}
215+
216+
217+
def validate_html_dictionary(data):
218+
for key, value in data.items():
219+
# Do not validate HTML key
220+
if key == "HTML":
221+
continue
222+
223+
# Recursive case for nested dictionaries
224+
if isinstance(value, dict):
225+
# Specific checks for properties key
226+
if key == "properties":
227+
subdict = {k: v for k, v in value.items() if k not in PROPERTIES_EXEMPT}
228+
validate_html_dictionary(subdict)
229+
else:
230+
validate_html_dictionary(value)
231+
232+
# Check for lists
233+
elif isinstance(value, list):
234+
validate_html_list(value, key)
235+
236+
# Main check for strings
237+
elif isinstance(value, str):
238+
cleaned_string = bleach.clean(value, strip=True)
239+
if cleaned_string != value:
240+
raise ValueError(f"{key} contains HTML content.")
241+
242+
# Ignore other types
243+
else:
244+
continue
245+
246+
247+
def validate_html_list(value_list, key):
248+
for item in value_list:
249+
if isinstance(item, str):
250+
cleaned_string = bleach.clean(item, strip=True)
251+
if cleaned_string != item:
252+
raise ValueError(f"{key} contains HTML content.")
253+
elif isinstance(item, dict):
254+
validate_html_dictionary(item)
255+
elif isinstance(item, list):
256+
validate_html_list(item, key)
257+
else:
258+
continue

src/ansys/dynamicreporting/core/serverless/template.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.template.loader import render_to_string
99
from django.utils import timezone
1010

11+
from ..common_utils import validate_html_dictionary
1112
from ..constants import JSON_ATTR_KEYS
1213
from ..exceptions import ADRException, TemplateDoesNotExist, TemplateReorderOutOfBounds
1314
from .base import BaseModel, StrEnum
@@ -251,6 +252,8 @@ def set_params(self, new_params: dict) -> None:
251252
new_params = {}
252253
if not isinstance(new_params, dict):
253254
raise TypeError("input must be a dictionary")
255+
if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1":
256+
validate_html_dictionary(new_params)
254257
self.params = json.dumps(new_params)
255258

256259
def add_params(self, new_params: dict):

src/ansys/dynamicreporting/core/utils/report_objects.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import pytz
2323

2424
from . import extremely_ugly_hacks, report_utils
25+
from ..common_utils import validate_html_dictionary
2526
from ..exceptions import TemplateDoesNotExist, TemplateReorderOutOfBounds
2627
from .encoders import PayloaddataEncoder
2728

@@ -413,6 +414,8 @@ def set_params(self, d: dict = None):
413414
d = {}
414415
if type(d) is not dict:
415416
raise ValueError("Error: input must be a dictionary")
417+
if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1":
418+
validate_html_dictionary(d)
416419
self.params = json.dumps(d)
417420
return
418421

@@ -1541,6 +1544,8 @@ def set_params(self, d: dict = None):
15411544
d = {}
15421545
if type(d) is not dict:
15431546
raise ValueError("Error: input must be a dictionary")
1547+
if os.getenv("ADR_VALIDATION_BETAFLAG_ANSYS") == "1":
1548+
validate_html_dictionary(d)
15441549
self.params = json.dumps(d)
15451550
return
15461551

tests/test_report_objects.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import json
3+
import os
34
import uuid
45

56
import pytest
@@ -1964,6 +1965,62 @@ def test_unit_template() -> None:
19641965
assert succ and succ_two and succ_three and succ_four
19651966

19661967

1968+
@pytest.mark.ado_test
1969+
def test_template_validation() -> None:
1970+
os.environ["ADR_VALIDATION_BETAFLAG_ANSYS"] = "1"
1971+
a = ro.Template()
1972+
try:
1973+
a.set_params(
1974+
{
1975+
"reduce_params": {
1976+
"reduce_type": "row<script>This is bad</script>",
1977+
"operations": [
1978+
"test 1",
1979+
["test 2", 1],
1980+
{"source_rows": "'Phase*'", "output_rows": "Maximum"},
1981+
],
1982+
},
1983+
"properties": {"plot": "line", "plot_title": "Reduced Table"},
1984+
"HTML": "<div>Test</div>",
1985+
}
1986+
)
1987+
except ValueError as e:
1988+
succ_one = "contains HTML content" in str(e)
1989+
try:
1990+
a.set_params(
1991+
{
1992+
"reduce_params": {
1993+
"reduce_type": "row",
1994+
"operations": [
1995+
["test 2", 1],
1996+
{"source_rows": "'Phase*'", "output_rows": "Maximum"},
1997+
"test 1<script>Bad</script>",
1998+
],
1999+
},
2000+
"properties": {"plot": "line", "plot_title": "Reduced Table"},
2001+
"HTML": "<div>Test</div>",
2002+
}
2003+
)
2004+
except ValueError as e:
2005+
succ_two = "contains HTML content" in str(e)
2006+
a.set_params(
2007+
{
2008+
"reduce_params": {
2009+
"reduce_type": "row",
2010+
"operations": [
2011+
"test 1",
2012+
["test 2", 1],
2013+
{"source_rows": "'Phase*'", "output_rows": "Maximum"},
2014+
],
2015+
},
2016+
"HTML": "<div>Test</div>",
2017+
"properties": {"plot": "line", "plot_title": "Reduced Table<script>Bad</script>"},
2018+
}
2019+
)
2020+
del os.environ["ADR_VALIDATION_BETAFLAG_ANSYS"]
2021+
assert succ_one and succ_two
2022+
2023+
19672024
@pytest.mark.ado_test
19682025
def test_unit_base() -> None:
19692026
a = ro.BaseRESTObject()

0 commit comments

Comments
 (0)