fix(posting-guard): read style values like a browser and treat a collapsed, clipped box as hidden (#709)

* fix(posting-guard): read style values like a browser and treat a collapsed, clipped box as hidden

The hidden-style regex ran on the raw attribute value, so
"display:none" (entity-encoded colon) and "display:/**/none" (a CSS
comment inside the declaration) both hid the content in a browser while
the guard showed it under a clean verdict. A zero-height or zero-width
box with overflow:hidden, the other common way to hide a block, was not
a hidden style at all.

Entity-decode and comment-strip the value before matching, and count a
collapsed box as hidden only when both halves are present: overflow:
hidden alone is on most cards and height:0 alone is a spacer. Three new
Bypasses cases (seven subtests fail on master) plus a calibration case
pinning that either half alone stays visible.

* changelog: place the #709 entry under [Unreleased]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-21 07:19:47 +02:00
1 parent e608daf80c
commit b67eb9e05a
3 files changed
+59 -3

No files matched your search

+26 -3
View File
@@ -8,8 +8,10 @@ makes the obvious payload classes visible deterministically, before Step 1
evaluates anything:
* hidden HTML (display:none, visibility:hidden, opacity:0, font-size:0,
color:transparent, clip/clip-path, off-screen positioning, the `hidden`
attribute, aria-hidden) - removed from the text; its content is scanned,
color:transparent, clip/clip-path, off-screen positioning, a zero-height
or zero-width box with overflow:hidden, the `hidden` attribute,
aria-hidden; the style value is read the way a browser reads it, with
entities decoded and CSS comments removed) - removed from the text; its content is scanned,
and reported as excerpts under `hidden_blocks`. Comments and
script/style/noscript/svg/template blocks are removed and scanned too but
not listed (every page has them);
@@ -202,11 +204,32 @@ def _attrs(raw: str) -> dict:
return out
CSS_COMMENT = re.compile(r"/\*.*?\*/", re.S)
# A box collapsed to nothing with its overflow clipped hides its content as
# surely as display:none. Either half alone is everyday layout CSS (overflow:
# hidden is on most cards; height:0 is a spacer), so both must be present.
COLLAPSED_BOX = re.compile(r"\b(?:max-)?(?:height|width)\s*:\s*0(?:\.0+)?(?:px|em|rem|%|pt)?(?![\d.])", re.I)
CLIPPED = re.compile(r"\boverflow(?:-[xy])?\s*:\s*hidden\b", re.I)
def _style(attrs: dict) -> str:
"""The style attribute as a browser reads it: entities decoded, CSS comments gone.
A browser decodes `display&#58;none` before parsing the CSS and ignores
`/**/` inside a declaration, so both hid the content while the regex saw
neither. Normalise before matching.
"""
return CSS_COMMENT.sub("", html.unescape(attrs.get("style", "")))
def _is_hidden(raw_attrs: str) -> bool:
attrs = _attrs(raw_attrs)
if "hidden" in attrs or attrs.get("aria-hidden", "").strip().lower() == "true":
return True
return bool(HIDDEN_STYLE.search(attrs.get("style", "")))
style = _style(attrs)
if HIDDEN_STYLE.search(style):
return True
return bool(COLLAPSED_BOX.search(style) and CLIPPED.search(style))
def clean_html(markup: str):