diff --git a/CHANGELOG.md b/CHANGELOG.md index f5e760b..f6294fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ Versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [0.19.3] - Unreleased + +### Fixed + +- **Security:** Resolved a Z205 bypass vulnerability where maliciously encoded HTML entities or duplicated href attributes could evade the forbidden scheme detection. + ## [0.19.2] — 2026-07-02 ### Fixed diff --git a/src/zenzic/core/validator.py b/src/zenzic/core/validator.py index 0088555..dd0178d 100644 --- a/src/zenzic/core/validator.py +++ b/src/zenzic/core/validator.py @@ -29,6 +29,7 @@ import concurrent.futures import difflib import fnmatch +import html import json import os import sys @@ -224,6 +225,9 @@ class LinkInfo(NamedTuple): # Pattern fence per PolyglotExtractor._mask_fences (subset di SuppressionTracker). _POLY_FENCE_RE: re.RegexPattern = re.compile(r"^(?P[`~]{3,})") +# Strip whitespaces and control characters from URLs prima del check Z205. +_POLY_CLEAN_URL_RE: re.RegexPattern = re.compile(r"[\s\x00-\x1F]+") + @dataclass(frozen=True, slots=True) class HtmlNodeInfo: @@ -355,12 +359,17 @@ def _parse_node(self, tag: str, attrs_str: str, line_no: int, raw_tag: str) -> H suppressed = False unknown: list[str] = [] blacklisted: list[str] = [] + seen_attrs: set[str] = set() for m in _RE_POLY_ATTR.finditer(attrs_str): key_raw = m.group("key") if not key_raw: continue key = key_raw.lower() + if key in seen_attrs: + continue + seen_attrs.add(key) + val_raw = m.group("val") or "" val = val_raw.strip("\"'") @@ -379,10 +388,11 @@ def _parse_node(self, tag: str, attrs_str: str, line_no: int, raw_tag: str) -> H # ── Security Gate Z205: check PRIMA di data-zenzic-ignore ───────────────── z205_scheme: str | None = None + clean_href: str | None = None if href: - href_lower = href.lower().lstrip() + clean_href = _POLY_CLEAN_URL_RE.sub("", html.unescape(href)).lower() for scheme in _POLY_FORBIDDEN_SCHEMES: - if href_lower.startswith(scheme): + if clean_href.startswith(scheme): z205_scheme = scheme break @@ -390,10 +400,9 @@ def _parse_node(self, tag: str, attrs_str: str, line_no: int, raw_tag: str) -> H is_missing_href = href is None is_jump_link = href == "#" info_scheme: str | None = None - if href and not is_jump_link and z205_scheme is None: - href_lower_info = href.lower() + if clean_href and not is_jump_link and z205_scheme is None: for scheme in _POLY_INFO_SCHEMES: - if href_lower_info.startswith(scheme): + if clean_href.startswith(scheme): info_scheme = scheme break diff --git a/tests/fixtures/z205-bypass-test.md b/tests/fixtures/z205-bypass-test.md new file mode 100644 index 0000000..ecbeaf4 --- /dev/null +++ b/tests/fixtures/z205-bypass-test.md @@ -0,0 +1,13 @@ +# Z205 Bypass Payloads + +Questi payload servono a verificare che il parser prevenga il bypass dell'attributo security gate (Z205) tramite Parser Differential e Injection. + + +Double Href Attack + + +HTML Entities + + +Whitespace +Control Chars