diff --git a/src/ydnatl/core/element.py b/src/ydnatl/core/element.py index fc601b0..f93538f 100644 --- a/src/ydnatl/core/element.py +++ b/src/ydnatl/core/element.py @@ -2,6 +2,7 @@ import copy import os import functools +import html from typing import Callable, Any, Iterator, Union, List @@ -225,7 +226,7 @@ def self_closing(self, value: bool) -> None: def _render_attributes(self) -> str: """Returns a string of HTML attributes for the tag.""" attr_str = " ".join( - f'{("class" if k == "class_name" else k)}="{v}"' + f'{("class" if k == "class_name" else k)}="{html.escape(str(v), quote=True)}"' for k, v in self._attributes.items() ) return f" {attr_str}" if attr_str else "" @@ -240,7 +241,8 @@ def render(self) -> str: result = f"{tag_start} />" else: children_html = "".join(child.render() for child in self._children) - result = f"{tag_start}>{self._text}{children_html}" + escaped_text = html.escape(self._text) + result = f"{tag_start}>{escaped_text}{children_html}" if hasattr(self, "_prefix") and self._prefix: result = f"{self._prefix}{result}" diff --git a/tests/test_element.py b/tests/test_element.py index 4fbee52..1b5e007 100644 --- a/tests/test_element.py +++ b/tests/test_element.py @@ -218,6 +218,32 @@ def test_to_dict(self): self.assertIsInstance(el1.to_dict(), dict) self.assertEqual(el1.to_dict(), should_be) + def test_html_escaping_text_content(self): + """Test that text content is properly escaped to prevent XSS.""" + malicious_text = '' + element = HTMLElement(malicious_text, tag="div") + rendered = str(element) + self.assertIn("<script>", rendered) + self.assertIn("</script>", rendered) + self.assertNotIn("", rendered) + + def test_html_escaping_attribute_values(self): + """Test that attribute values are properly escaped to prevent XSS.""" + malicious_attr = '">