From ec7536b8d257f80f2cf60aeb028e9004adeffbf1 Mon Sep 17 00:00:00 2001 From: Dmitry Ermakovich Date: Thu, 10 Sep 2026 16:07:13 +0300 Subject: [PATCH] fix: TMS-414992 fix xss html parameters issue --- testit-adapter-behave/setup.py | 2 +- testit-adapter-nose/setup.py | 2 +- testit-adapter-pytest/setup.py | 2 +- .../tests/test_xss_parametrize_escape.py | 39 ++++++++ testit-adapter-robotframework/setup.py | 2 +- testit-python-commons/setup.py | 2 +- .../client/api_client.py | 4 + .../client/helpers/bulk_autotest_helper.py | 6 +- .../models/test_result.py | 2 +- .../utils/html_escape_utils.py | 98 ++++++++++++------- .../tests/test_html_escape_utils.py | 45 +++++++++ .../tests/test_xss_parameters_escape.py | 72 ++++++++++++++ update_versions.sh | 2 +- 13 files changed, 233 insertions(+), 45 deletions(-) create mode 100644 testit-adapter-pytest/tests/test_xss_parametrize_escape.py create mode 100644 testit-python-commons/tests/test_xss_parameters_escape.py diff --git a/testit-adapter-behave/setup.py b/testit-adapter-behave/setup.py index da4572ed..1a4f9697 100644 --- a/testit-adapter-behave/setup.py +++ b/testit-adapter-behave/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "5.2.0" +VERSION = "5.2.1" setup( name='testit-adapter-behave', diff --git a/testit-adapter-nose/setup.py b/testit-adapter-nose/setup.py index 728c82ab..2bddff7a 100644 --- a/testit-adapter-nose/setup.py +++ b/testit-adapter-nose/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = "5.2.0" +VERSION = "5.2.1" setup( name='testit-adapter-nose', diff --git a/testit-adapter-pytest/setup.py b/testit-adapter-pytest/setup.py index afc0a97d..eaf4b274 100644 --- a/testit-adapter-pytest/setup.py +++ b/testit-adapter-pytest/setup.py @@ -1,6 +1,6 @@ from setuptools import find_packages, setup -VERSION = "5.2.0" +VERSION = "5.2.1" setup( name='testit-adapter-pytest', diff --git a/testit-adapter-pytest/tests/test_xss_parametrize_escape.py b/testit-adapter-pytest/tests/test_xss_parametrize_escape.py new file mode 100644 index 00000000..86256c3c --- /dev/null +++ b/testit-adapter-pytest/tests/test_xss_parametrize_escape.py @@ -0,0 +1,39 @@ +"""Ensure pytest parametrize XSS values are escaped in TestResult / API payload.""" + +from testit_adapter_pytest.models.executable_test import ExecutableTest +from testit_adapter_pytest.utils import convert_executable_test_to_test_result_model +from testit_python_commons.client.converter import Converter +from adapters_api import ApiClient + + +def test_parametrize_script_payload_escaped_in_parameters_and_external_key(): + payload = '' + executable = ExecutableTest( + external_id="xss_ext", + name=f"with xss {payload}", + duration=1, + parameters={"description": payload}, + properties={}, + namespace="tests", + classname=None, + title=None, + description=None, + links=[], + labels=[], + tags=[], + work_item_ids=[], + node_id=f"tests/test_xss_parametrize.py::test_05_with_chg_xss[{payload}]", + outcome="Passed", + status_type="Succeeded", + ) + + test_result = convert_executable_test_to_test_result_model(executable) + assert test_result.get_parameters()["description"] == '<script>alert("1")</script>' + assert ""), + SampleData("Second", ""), + ] + result = HtmlEscapeUtils.escape_html_in_object(test_list) + + self.assertEqual(result[0].description, "<script>alert(1)</script>") + self.assertEqual(result[1].description, "<img src=x onerror=alert(1)>") + + def test_escape_html_in_list_of_strings(self): + tags = ["", "safe", "x"] + result = HtmlEscapeUtils.escape_html_in_object(tags) + + self.assertEqual(result[0], "<script>alert(1)</script>") + self.assertEqual(result[1], "safe") + self.assertEqual(result[2], "<b>x</b>") + + def test_escape_html_in_list_of_dicts(self): + labels = [{"name": ""}, {"name": "ok"}] + result = HtmlEscapeUtils.escape_html_in_object(labels) + + self.assertEqual(result[0]["name"], "<script>alert(1)</script>") + self.assertEqual(result[1]["name"], "ok") + def test_escape_disabled_by_env_var(self): """Test that escaping can be disabled via environment variable""" os.environ[HtmlEscapeUtils.NO_ESCAPE_HTML_ENV_VAR] = "true" @@ -131,6 +157,25 @@ def test_escape_html_in_dictionary(self): self.assertEqual(result["tags"][0], "<tag>") self.assertEqual(result["tags"][1], "normal_tag") + def test_escape_openapi_model_parameters(self): + """OpenAPI models hide fields from dir(); parameters must still be escaped.""" + from adapters_api.models import AutoTestResultsForTestRunModel, TestStatusType + + model = AutoTestResultsForTestRunModel( + configuration_id="cfg", + auto_test_external_id="ext", + status_type=TestStatusType("Succeeded"), + parameters={"description": ''}, + message='', + ) + HtmlEscapeUtils.escape_html_in_object(model) + + self.assertEqual( + model.parameters["description"], + '<script>alert("1")</script>', + ) + self.assertEqual(model.message, "<img src=x onerror=alert(1)>") + if __name__ == '__main__': unittest.main() diff --git a/testit-python-commons/tests/test_xss_parameters_escape.py b/testit-python-commons/tests/test_xss_parameters_escape.py new file mode 100644 index 00000000..921a568c --- /dev/null +++ b/testit-python-commons/tests/test_xss_parameters_escape.py @@ -0,0 +1,72 @@ +"""Regression: XSS in pytest params must be escaped before TMS API payloads.""" + +import importlib.util +import unittest +from pathlib import Path + +from adapters_api import ApiClient +from testit_python_commons.client.converter import Converter +from testit_python_commons.models.test_result import TestResult +from testit_python_commons.utils.html_escape_utils import HtmlEscapeUtils + + +def _load_xss_payloads(): + path = Path(__file__).resolve().parents[2] / "test_files" / "test_xss_parametrize.py" + spec = importlib.util.spec_from_file_location("test_xss_parametrize", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.XSS_PAYLOADS + + +class TestXssParametersEscape(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.payloads = _load_xss_payloads() + + def test_test_result_parameters_escaped_for_all_payloads(self): + for payload in self.payloads: + with self.subTest(payload=payload): + result = TestResult().set_parameters({"description": payload}) + value = result.get_parameters()["description"] + if HtmlEscapeUtils._HTML_TAG_PATTERN.search(payload): + self.assertNotIn("", escaped) + + def test_bulk_list_escape_mutates_result_models(self): + payload = '' + # Simulate model built WITHOUT going through TestResult setters (raw params) + class FakeResult: + def __init__(self): + self.parameters = {"description": payload} + self.message = payload + + models = [FakeResult(), FakeResult()] + HtmlEscapeUtils.escape_html_in_object(models) + for model in models: + self.assertEqual( + model.parameters["description"], + '<script>alert("1")</script>', + ) + self.assertEqual(model.message, '<script>alert("1")</script>') + + +if __name__ == "__main__": + unittest.main() diff --git a/update_versions.sh b/update_versions.sh index d79a943f..32217d4c 100644 --- a/update_versions.sh +++ b/update_versions.sh @@ -1,6 +1,6 @@ #!/bin/bash -NEW_VERSION="5.2.0" +NEW_VERSION="5.2.1" echo "Updating all adapters to version: $NEW_VERSION"