feat(consent-tester): cookieless Opt-out erkennen statt False-HIGHs

Cookie-freie Analyse mit reinem Opt-out-Hinweis (z.B. bayshore.ai:
"Privacy-friendly, cookie-free analytics are currently enabled ... Disable")
ist KEIN Consent-Banner: cookieless = kein Endgeräte-Zugriff → §25 TDDDG
verlangt keine Einwilligung → Opt-out statt Opt-in. Die Standard-Opt-in-
Checks (granulare Kategorien, Accept/Reject-Balance, Impressum-im-Banner)
trafen nicht zu und erzeugten 3 Falsch-HIGHs.

is_cookieless_optout() erkennt das Muster (cookieless-Signal + Opt-out-Wort,
KEIN Consent-Signal); check_banner_text gibt dann früh EINEN ausführlichen
LOW-Erklär-Befund zurück (zählt nicht als HIGH) und setzt die Opt-in-Checks
aus. Ausführlich, weil der Fall extrem untypisch ist.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-12 19:27:12 +02:00
parent 2f68646c2d
commit d8a9e3049d
2 changed files with 114 additions and 0 deletions
@@ -27,6 +27,73 @@ from services.banner_dom_walkers import (
logger = logging.getLogger(__name__)
# ── Cookieless Opt-out-Erkennung (sehr untypischer Sonderfall) ──────────
# Manche Sites nutzen cookie-FREIE Analyse mit einem reinen Opt-out-Hinweis
# statt eines Consent-Banners (z.B. bayshore.ai: "Privacy-friendly, cookie-free
# analytics are currently enabled. You can change your choice at any time."
# + ein einziger Button "Disable"). Cookieless = kein Zugriff aufs Endgeraet →
# §25 Abs.1 TDDDG/ePrivacy verlangt KEINE Einwilligung → Opt-out (default an,
# abschaltbar) ist zulaessig statt Opt-in. Die Standard-Opt-in-Checks (granulare
# Kategorien, Accept/Reject-Balance, Impressum-im-Banner, Dark-Pattern-Buttons)
# treffen hier NICHT zu → sonst False Positives.
_CL_SIGNAL = (
"cookie-free", "cookieless", "cookie free", "without cookies", "no cookies",
"ohne cookies", "cookielos", "privacy-friendly analytics",
"privacy friendly analytics", "cookie-freie analyse", "cookiefreie analyse",
"datenschutzfreundliche analyse",
)
_CL_OPTOUT = (
"disable", "deaktivieren", "opt-out", "opt out", "abschalten", "ausschalten",
"currently enabled", "widersprechen", "change your choice",
)
# Wenn ein echtes Consent-Banner-Signal da ist, ist es KEIN reiner Opt-out-Hinweis.
_CL_STD_CONSENT = (
"alle akzeptieren", "accept all", "alle annehmen", "accept cookies",
"cookies akzeptieren", "zustimmen", "einwilligen",
)
def is_cookieless_optout(banner_lower: str) -> bool:
"""True bei cookie-freier Analyse mit reinem Opt-out-Hinweis (kein
Consent-Banner). Pure + testbar. Standard-Opt-in-Checks treffen dann nicht zu."""
return (
any(s in banner_lower for s in _CL_SIGNAL)
and any(o in banner_lower for o in _CL_OPTOUT)
and not any(c in banner_lower for c in _CL_STD_CONSENT)
)
def _cookieless_finding(has_dse: bool) -> Violation:
"""Vollstaendiger Erklaer-Befund (LOW) fuer den cookieless-Opt-out-Fall —
bewusst ausfuehrlich, weil der Fall extrem untypisch ist und ein DSB/Nutzer
die Einordnung sonst nicht versteht."""
return Violation(
service="Cookie-Banner",
severity="LOW",
text=(
"Cookieless Analytics im Opt-out-Modell erkannt — KEIN klassisches "
"Consent-Banner. Der Hinweis bezieht sich auf datenschutzfreundliche, "
"cookie-freie Analyse: sie arbeitet OHNE Cookies und OHNE persistente "
"Kennung (typisch ein taeglich zurueckgesetzter Hash aus IP+Browser), "
"speichert keine personenbezogenen Daten und verfolgt nicht ueber "
"Sitzungen oder Geraete hinweg. Weil dabei NICHT auf das Endgeraet "
"zugegriffen wird, greift die Einwilligungspflicht nach §25 Abs.1 TDDDG "
"(ePrivacy) NICHT — deshalb ist Opt-out (standardmaessig aktiv, jederzeit "
"ueber 'Disable' abschaltbar) zulaessig statt Opt-in. Die ueblichen "
"Opt-in-Pruefungen (granulare Kategorien, Accept/Reject-Gleichwertigkeit, "
"Impressum-Link im Banner, Dark-Pattern-Buttons) sind hier NICHT anwendbar "
"und werden bewusst ausgesetzt, um Falschbefunde zu vermeiden. Bewertung: "
"wahrscheinlich konform und sogar datenschutzfreundlicher als ein Standard-"
"Cookie-Banner. Zu pruefen bleibt nur: funktioniert der Opt-out wirklich "
"(Analyse stoppt nach 'Disable') und ist eine Datenschutzerklaerung verlinkt"
+ ("." if has_dse else " — ein DSE-Link fehlt im Hinweis.")
),
legal_ref="§25 Abs. 1 TDDDG (cookieless ohne Endgeraete-Zugriff = keine "
"Einwilligungspflicht), ErwGr. 30 DSGVO, EDPB Guidelines 05/2020 "
"(Anwendungsbereich der Einwilligung)",
)
async def check_banner_text(page) -> dict:
"""Check cookie banner text for legal issues.
@@ -86,6 +153,19 @@ async def check_banner_text(page) -> dict:
banner_lower = banner_text.lower()
# Sonderfall cookieless Opt-out (sehr untypisch): erkennen, vollstaendig
# erklaeren und die Opt-in-Checks aussetzen — statt Falsch-HIGHs zu erzeugen.
if is_cookieless_optout(banner_lower):
has_dse = any(
"datenschutz" in l["href"] or "datenschutz" in l["text"]
or "privacy" in l["href"] or "privacy" in l["text"]
or "dsgvo" in l["href"]
for l in banner_links
)
return {"violations": [_cookieless_finding(has_dse)],
"has_impressum": False, "has_dse": has_dse,
"cookieless_optout": True}
# Check 1: Impressum link in or accessible through banner
has_impressum = any(
"impressum" in l["href"] or "impressum" in l["text"] or
@@ -0,0 +1,34 @@
"""Cookieless Opt-out-Erkennung im banner_text_checker.
Sehr untypischer Sonderfall: cookie-FREIE Analyse mit reinem Opt-out-Hinweis
statt Consent-Banner (z.B. bayshore.ai). Standard-Opt-in-Checks duerfen dann
NICHT feuern (sonst False Positives).
"""
from services.banner_text_checker import is_cookieless_optout
def test_bayshore_cookieless_optout_detected():
# Realer bayshore.ai-Bannertext (Opt-out fuer cookie-freie Analyse).
bay = ("privacy-friendly, cookie-free analytics are currently enabled. "
"you can change your choice at any time. disable")
assert is_cookieless_optout(bay) is True
def test_standard_consent_banner_not_cookieless():
assert not is_cookieless_optout(
"wir nutzen cookies. alle akzeptieren ablehnen einstellungen")
def test_cookiefree_but_with_accept_is_not_optout():
# 'cookie-free' genannt, aber echtes Consent ('accept all') → kein reiner Opt-out.
assert not is_cookieless_optout("cookie-free analytics. accept all disable")
def test_signal_without_optout_word_is_not_detected():
# cookie-free, aber kein Opt-out-Mechanismus im Text.
assert not is_cookieless_optout("cookie-free analytics enabled")
def test_empty():
assert not is_cookieless_optout("")