""" P103 — Cookie-Value-Entropy-Check (Stufe 3). Bewertet ob der Cookie-Wert zur deklarierten Kategorie passt: * "Funktional" + 2-char-Wert ('1', 'de') → konsistent (Flag) * "Funktional" + 64-char-Base64 → INKONSISTENT (Tracking-ID-Pattern) * "Marketing" + 32+ char Hash → konsistent * "Marketing" + 2-char-Wert → konsistent (Boolean-Opt-Out) Defeat-Device-Pattern: Site deklariert "Funktional" um Consent zu umgehen, aber Wert sieht wie pseudonymisierte Tracking-ID aus. """ from __future__ import annotations import logging import math import re logger = logging.getLogger(__name__) def _shannon_entropy(s: str) -> float: if not s: return 0.0 from collections import Counter n = len(s) counts = Counter(s) return -sum((c / n) * math.log2(c / n) for c in counts.values()) _BASE64_RE = re.compile(r"^[A-Za-z0-9+/=_-]{20,}$") _HEX_RE = re.compile(r"^[a-fA-F0-9]{16,}$") _UUID_RE = re.compile( r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-" r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" ) _FLAG_VALUES = {"0", "1", "true", "false", "yes", "no", "de", "en", "de-de", "en-us", "fr-fr", "accept", "deny", "essential", "on", "off"} def _classify_value_shape(value: str) -> str: """Returns one of: 'flag', 'short_id', 'long_token', 'uuid', 'hash', 'json_blob', 'unknown'.""" if not value: return "flag" v = value.strip() if v.lower() in _FLAG_VALUES: return "flag" if len(v) <= 4: return "flag" if _UUID_RE.match(v): return "uuid" if _HEX_RE.match(v) and len(v) >= 32: return "hash" if _BASE64_RE.match(v) and len(v) >= 40: return "long_token" if v.startswith("{") or v.startswith("["): return "json_blob" if len(v) >= 16 and _shannon_entropy(v) > 3.5: return "long_token" if len(v) >= 6: return "short_id" return "flag" def check_cookies_for_entropy_mismatch( cookies_detailed: list[dict] | None, ) -> list[dict]: """Liefert Findings fuer Cookies deren Wert-Shape nicht zur deklarierten Kategorie passt.""" out: list[dict] = [] if not cookies_detailed: return out for ck in cookies_detailed: if not isinstance(ck, dict): continue name = (ck.get("name") or "").strip() value = (ck.get("value") or "").strip() declared = (ck.get("declared_category") or "").lower().strip() if not name or not declared: continue shape = _classify_value_shape(value) # Regel: 'essential' / 'functional' Cookies mit hoher # Tracking-ID-Komplexitaet sind verdaechtig. is_low_cat = declared in ("essential", "functional", "necessary") is_id_shape = shape in ("uuid", "hash", "long_token") if is_low_cat and is_id_shape: out.append({ "cookie": name, "declared": declared, "value_shape": shape, "value_len": len(value), "severity": "MEDIUM", "label": ( f"Cookie '{name}' deklariert als '{declared}', " f"aber Wert ist ein {shape} ({len(value)} Zeichen) — " "typisches Tracking-ID-Pattern" ), "detail": ( "Funktionale/notwendige Cookies speichern normalerweise " "kurze Flags (1, true, de-DE). Ein langer Hash/UUID-Wert " "in einem als 'essential' deklarierten Cookie ist ein " "Indikator fuer verstecktes Tracking — vergleichbar mit " "einem 'Defeat Device', das auf dem Pruefstand harmlos " "aussieht aber im Realbetrieb anderes tut." ), }) return out def build_entropy_block_html(findings: list[dict]) -> str: if not findings: return "" items: list[str] = [] for f in findings[:25]: items.append( f'
  • ' f'{f["cookie"]} ' f'(deklariert: ' f'{f["declared"]}) — Wert-Shape: ' f'' f'{f["value_shape"]} ' f'({f["value_len"]} Zeichen)' f'
  • ' ) return ( '
    ' '
    ' 'Cookie-Werte-Plausibilitaet (Defeat-Device-Heuristik)
    ' f'

    ' f'{len(findings)} Cookie{"s" if len(findings) != 1 else ""} ' 'mit verdaechtigem Wert-Pattern

    ' '

    ' 'Diese Cookies sind als "essential" oder "funktional" deklariert, ' 'ihr tatsaechlicher Wert sieht aber wie eine Tracking-ID aus ' '(UUID, Hash, langer Base64-Token). Empfehlung: pruefen ob diese ' 'Cookies wirklich nur technisch notwendig sind oder de facto ' 'pseudonymisierte User-Tracker.

    ' '
    ' )