From 7c137f4b750dfccc945ea06007ddd69e269a7d46 Mon Sep 17 00:00:00 2001 From: PythonWoods-Dev Date: Sat, 4 Jul 2026 10:02:17 +0200 Subject: [PATCH] fix(core/validator): harden polyglot extractor comment masking and fence evasion Signed-off-by: PythonWoods-Dev --- src/zenzic/core/validator.py | 17 +++++++++--- tests/test_fix.py | 51 ++++++++++++++++++++++++++++++++++++ uv.lock | 2 +- 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/zenzic/core/validator.py b/src/zenzic/core/validator.py index 1a5fe9d..0b6e278 100644 --- a/src/zenzic/core/validator.py +++ b/src/zenzic/core/validator.py @@ -223,7 +223,11 @@ class LinkInfo(NamedTuple): _POLY_INFO_SCHEMES: frozenset[str] = frozenset({"mailto:", "tel:", "ftp:"}) # Pattern fence per PolyglotExtractor._mask_fences (subset di SuppressionTracker). -_POLY_FENCE_RE: re.RegexPattern = re.compile(r"^(?P[`~]{3,})") +_POLY_FENCE_RE: re.RegexPattern = re.compile(r"^(?P[`~]{3,})(?P.*)$") + +# HTML and MDX Comment Regex Patterns for masking +_POLY_COMMENT_RE: re.RegexPattern = re.compile(r"", re.DOTALL) +_POLY_MDX_COMMENT_RE: re.RegexPattern = re.compile(r"\{\/\*.*?\*\/\}", re.DOTALL) # Strip whitespaces and control characters from URLs prima del check Z205. _POLY_CLEAN_URL_RE: re.RegexPattern = re.compile(r"[\s\x00-\x1F]+") @@ -294,7 +298,7 @@ def extract(self, text: str) -> list[HtmlNodeInfo]: Lista di :class:`HtmlNodeInfo`, uno per ogni tag ````/```` trovato fuori dai blocchi di codice. """ - masked = self._mask_inline_code(self._mask_fences(text)) + masked = self._mask_inline_code(self._mask_fences(self._mask_comments(text))) nodes: list[HtmlNodeInfo] = [] for m in _RE_POLY_TAG.finditer(masked): tag = m.group(1).lower() @@ -304,6 +308,12 @@ def extract(self, text: str) -> list[HtmlNodeInfo]: nodes.append(self._parse_node(tag, attrs_str, line_no, m.group(0))) return nodes + def _mask_comments(self, text: str) -> str: + """Mask HTML and MDX comments with spaces of equal length to preserve offsets.""" + text = _POLY_COMMENT_RE.sub(lambda m: " " * len(m.group(0)), text) + text = _POLY_MDX_COMMENT_RE.sub(lambda m: " " * len(m.group(0)), text) + return text + def _mask_inline_code(self, text: str) -> str: """Sostituisce blocchi inline code con spazi bianchi preservando gli offset.""" from zenzic.core.validator import _INLINE_CODE_RE @@ -338,7 +348,8 @@ def _mask_fences(self, text: str) -> str: else: if fm: fence = fm.group("fence") - if fence[0] == open_char and len(fence) >= open_len: + info = fm.group("info").strip() + if fence[0] == open_char and len(fence) >= open_len and not info: inside = False result.append(" " * len(line)) return "\n".join(result) diff --git a/tests/test_fix.py b/tests/test_fix.py index d1bd15f..2b3afac 100644 --- a/tests/test_fix.py +++ b/tests/test_fix.py @@ -80,3 +80,54 @@ def test_formatted_empty_link_validation_and_mutation() -> None: serialized = serialize(new_ast) assert serialized == "[MISSING LINK LABEL](url)", f"Got: {serialized}" + + +def test_polyglot_extractor_comment_masking() -> None: + """An HTML tag or a Z205 scheme inside an HTML or MDX comment is ignored by PolyglotExtractor.""" + from zenzic.core.validator import PolyglotExtractor + + extractor = PolyglotExtractor() + + # 1. HTML comment with Z205 scheme + text = "" + nodes = extractor.extract(text) + assert len(nodes) == 0, "Should ignore tags inside HTML comments" + + # 2. MDX comment with Z205 scheme + text = "{/* XSS */}" + nodes = extractor.extract(text) + assert len(nodes) == 0, "Should ignore tags inside MDX comments" + + # 3. HTML tag outside comments but comments present + text = "\nlink\n{/* comment */}" + nodes = extractor.extract(text) + assert len(nodes) == 1 + assert nodes[0].href == "safe.md" + assert nodes[0].line_no == 2 + + +def test_polyglot_extractor_fence_evasion() -> None: + """A closing fence with trailing characters is NOT recognized as a closing fence, keeping masking active.""" + from zenzic.core.validator import PolyglotExtractor + + extractor = PolyglotExtractor() + + # A fence is opened, then closed with ```extra. It should NOT be closed. + # Therefore, the tag inside/after it should remain masked. + text = ( + "```\ninside\n```extra\nafter-fake-close\n```\n" + ) + nodes = extractor.extract(text) + # The first tag is inside the block. The second tag is also inside the block because ```extra didn't close it. + # The last ``` finally closes the block. + # So both should be masked, resulting in 0 extracted nodes. + assert len(nodes) == 0, ( + "Should treat both tags as inside the code block because of malformed closing fence" + ) + + # If it is closed correctly (without extra info), the tag after should be extracted + text_closed = "```\ninside\n```\nafter-real-close\n" + nodes_closed = extractor.extract(text_closed) + assert len(nodes_closed) == 1 + assert nodes_closed[0].href == "safe.md" + assert nodes_closed[0].line_no == 4 diff --git a/uv.lock b/uv.lock index fcc5c40..a27d8cd 100644 --- a/uv.lock +++ b/uv.lock @@ -2465,7 +2465,7 @@ wheels = [ [[package]] name = "zenzic" -version = "0.19.3" +version = "0.19.4" source = { editable = "." } dependencies = [ { name = "google-re2" },