From d8a9e3049dcbfa2ee826f7a6464b575a1f9625a8 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 12 Jun 2026 19:27:12 +0200 Subject: [PATCH] feat(consent-tester): cookieless Opt-out erkennen statt False-HIGHs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../services/banner_text_checker.py | 80 +++++++++++++++++++ .../tests/test_cookieless_optout.py | 34 ++++++++ 2 files changed, 114 insertions(+) create mode 100644 consent-tester/tests/test_cookieless_optout.py diff --git a/consent-tester/services/banner_text_checker.py b/consent-tester/services/banner_text_checker.py index 31602f61..41a13098 100644 --- a/consent-tester/services/banner_text_checker.py +++ b/consent-tester/services/banner_text_checker.py @@ -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 diff --git a/consent-tester/tests/test_cookieless_optout.py b/consent-tester/tests/test_cookieless_optout.py new file mode 100644 index 00000000..5ff0aceb --- /dev/null +++ b/consent-tester/tests/test_cookieless_optout.py @@ -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("")