Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/zenzic/core/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<fence>[`~]{3,})")
_POLY_FENCE_RE: re.RegexPattern = re.compile(r"^(?P<fence>[`~]{3,})(?P<info>.*)$")

# 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]+")
Expand Down Expand Up @@ -294,7 +298,7 @@ def extract(self, text: str) -> list[HtmlNodeInfo]:
Lista di :class:`HtmlNodeInfo`, uno per ogni tag ``<a>``/``<img>``
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()
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
51 changes: 51 additions & 0 deletions tests/test_fix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "<!-- <a href='javascript:alert(1)'>XSS</a> -->"
nodes = extractor.extract(text)
assert len(nodes) == 0, "Should ignore tags inside HTML comments"

# 2. MDX comment with Z205 scheme
text = "{/* <a href='javascript:alert(1)'>XSS</a> */}"
nodes = extractor.extract(text)
assert len(nodes) == 0, "Should ignore tags inside MDX comments"

# 3. HTML tag outside comments but comments present
text = "<!-- comment -->\n<a href='safe.md'>link</a>\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 <a href="safe.md"> inside/after it should remain masked.
text = (
"```\n<a href='safe.md'>inside</a>\n```extra\n<a href='safe.md'>after-fake-close</a>\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 = "```\n<a href='safe.md'>inside</a>\n```\n<a href='safe.md'>after-real-close</a>\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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading