rebreak-monorepo/apps/rebreak-native/tmp/webcontent-layer2-research.md
chahinebrini b31066a04c feat(chat): native action sheet + Insta-style heart for DM messages
- ChatBubble: useActionSheet replaces custom Modal (native iOS popup, Android bottom sheet)
- DM mode (isDM prop): hides like-count, shows Insta-style heart badge under bubble when liked
- Group chat unchanged
- Cleanup: remove unused Modal/Platform imports, sheet styles, actionsOpen state
- deploy.sh: auto-detect ANDROID_HOME + auto-create local.properties for local Gradle
- NEXT_RELEASE.md: DM reactions release note
- Includes other staged work across binder-mac, marketing, ops/mdm, ios/
2026-05-30 09:14:32 +02:00

19 KiB
Raw Blame History

Layer 2 — ManagedSettings webContent-Filter als Always-On-Fallback

Recherche + Bewertung — iOS 26, rebreak-native. Stand: 2026-05-21. KEINE Code-Änderungen.


TL;DR / Empfehlung

Die Idee in der vorgeschlagenen Form (statische Top-50-Gambling-Domain-Liste, länderabhängig, „Always-On-Fallback wenn NEURLFilter aus") ist technisch machbar, aber strategisch schwach. Drei Kernbefunde:

  1. Es gibt KEINE Gambling-WebDomainCategory. WebContentSettings.FilterPolicy kennt nur .none / .specific / .auto / .all. .auto blockt ausschließlich Adult Content (Apple-Wortlaut). Eine Gambling-Kategorie existiert nirgends — also bleibt nur die manuelle 50er-Liste.
  2. Der „50-Domain-Cap" ist real und Apple-dokumentiert (nicht Projekt-Hypothese): „Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once." Steht wörtlich in der blockedByFilter-Doc und bei .specific(_:) / .auto(_:except:).
  3. Der „Fallback"-Nutzen ist fragwürdig, weil Layer 1 und Layer 2 dieselbe einzelne Schwachstelle teilen: wenn der User die Family-Controls-Authorization widerruft, fallen NICHT nur ManagedSettings, sondern faktisch der gesamte Tamper-Schutz weg. Der „NEURLFilter-off"-Fall, gegen den Layer 2 absichern soll, ist nur eine Teilmenge des größeren Lochs.

Empfehlung: Layer 2 nicht als 50er-Domain-Always-On-Fallback bauen. Stattdessen — falls überhaupt — als schmales Defense-in-Depth-Add-on: die Top-~50 Gambling-Domains des Nutzerlandes via webContent.blockedByFilter = .specific(...) setzen, erklärt als „Extra-Härtung", nicht als vollwertiger Fallback. Der reale Gewinn ist gering; der ehrliche Rat ist, die Energie eher in Bypass-Detection + Re-Aktivierungs-Nudges (existiert schon ansatzweise: recoveringFromBypass, /api/protection/state) zu stecken. Details + Entscheidungsfragen unten.


1. ManagedSettings.WebContentSettings-API — verifizierte Fakten

WebContentSettings

  • Struct, conformt ManagedSettingsGroup, verfügbar iOS 15.0+. Zugriff via ManagedSettingsStore().webContent.
  • Relevante Property:
    var blockedByFilter: WebContentSettings.FilterPolicy?
    
    „The current policy for filtering websites." Default nil (kein Effekt).

WebContentSettings.FilterPolicy — alle vier Cases (Apple-Doc, verifiziert)

case none                                         // kein Effekt
case specific(Set<WebDomain>)                     // blockt genau diese Domains
case auto(Set<WebDomain> = [], except: Set<WebDomain> = [])
                                                  // System blockt ADULT CONTENT
                                                  // (+ optional zusätzliche Domains, - Ausnahmen)
case all(except: Set<WebDomain>)                  // blockt ALLES außer Ausnahmen (Allowlist-Modus)

WebDomain

Token-Typ, der eine Domain repräsentiert (Initialisierung typ. mit WebDomain(domain: "bet365.com")). Set<WebDomain> ist das Argument bei .specific/.auto.

Gibt es eine Gambling-WebDomainCategory? — NEIN.

  • FilterPolicy hat keinen kategoriebasierten Case. Nur Adult-Content (.auto) ist kategorieähnlich, und das ist hardcoded auf Adult — nicht Gambling, nicht erweiterbar.
  • WebDomainCategory / webDomainCategories existiert — aber gehört zu ShieldSettings, nicht zum webContent-Filter. Und (Apple-Doc wörtlich): Shielding ist eine UI-Overlay-Funktion„the system calls your extension that customizes the shield's appearance". Shielding blockt keinen Traffic, es zeigt ein Overlay über bereits-erlaubten Apps/Domains. Für tatsächliches Web-Blocking irrelevant.
  • Fazit Punkt 1 der Aufgabe: Eine „Gambling-Kategorie statt 50er-Liste" gibt es auf iOS schlicht nicht. Die 50er-Liste ist der einzige Weg über webContent.

2. Der „50-Domain-Cap" — VERIFIZIERT, hart, Apple-dokumentiert

Frühere Projektannahme war korrekt. Apple-Doc-Wortlaut (blockedByFilter, .specific(_:), .auto(_:except:)):

„Your app can block up to 50 web domains and specify up to 50 web domains exceptions at once."

  • Gilt für .specific und .auto. Harte Obergrenze, keine Konfigurations-Option.
  • Damit ist eine 208k-Domain-Liste (wie bei NEURLFilter/PIR) über webContent prinzipiell unmöglich. Layer 2 ist API-bedingt auf eine kuratierte Top-50 beschränkt.
  • (Abzugrenzen von der separaten WebKit-Content-Blocker-Grenze von 50.000 Rules — das ist eine andere API und nicht gemeint.)

3. Wirkungsbereich — Safari sicher; restliche WebKit-Browser unbelegt

  • Belegt: webContent.blockedByFilter wirkt auf Safari — Apple erwähnt explizit den Nebeneffekt „Setting any filter policy besides .none will disable Safari private browsing."
  • Das ist ein systemweiter Screen-Time-Mechanismus (ManagedSettings = der „Enforcer" hinter Screen Time), kein App-lokaler Filter. Drittanbieter-Browser, die WebKit nutzen (auf iOS müssen das de facto alle), greifen mit hoher Wahrscheinlichkeit auf dieselbe Web-Content-Restriction zu — das ist auch das beobachtbare Screen-Time-Verhalten.
  • Hypothese, ungeprüft: Dass blockedByFilter auch in Chrome/Firefox/Drittanbieter-WKWebViews greift, ist plausibel (Screen-Time-Webcontent-Restriction wirkt klassischerweise browserübergreifend), aber nicht per Apple-Doc-Zitat belegt. Apple dokumentiert nur Safari namentlich. Vor Produktiv-Versprechen muss das auf dem iPhone-Build empirisch getestet werden (Chrome iOS → bet365 öffnen).

4. Family-Controls-Voraussetzung — ja, FC reicht; aber genau das ist die Crux

  • ManagedSettingsStore-Restriktionen wirken nur, wenn die App eine gültige Family-Controls-Authorization hat (AuthorizationCenter.shared.requestAuthorization(for: .individual).approved). Ohne Authorization sind ManagedSettings-Settings stumm.
  • Kein MDM nötig.individual-Authorization genügt. Das deckt sich exakt mit dem schon im Repo gebauten activateFamilyControls-Pfad (RebreakProtectionModule.swift, Z. 246296: FC-Auth → ManagedSettingsStore(...).application.denyAppRemoval = true).
  • Lokaler Xcode-Dev-Build: funktioniert mit dem Development-FC-Entitlement (Repo nutzt REBREAK_ENABLE_FAMILY_CONTROLS=1 im Plugin, Z. 65). v0.3.4 hat zusätzlich das Distribution-Entitlement (genehmigt) → auch TestFlight/Store ok.
  • Wichtige verifizierte Schwäche: „All ManagedSettingsStore restrictions are lifted immediately by the system when authorization is revoked, and the app receives no notification." Der User kann FC in Einstellungen → Bildschirmzeit widerrufen — dann ist Layer 2 lautlos weg, ohne Callback. Das untergräbt die „Always-On"-Behauptung.

5. Koexistenz NEURLFilter + ManagedSettings — unkritisch

  • Zwei getrennte Subsysteme: NEURLFilter = NetworkExtension (Netzwerk-Pfad), webContent = ManagedSettings/Screen-Time (WebKit-Restriction). Sie laufen auf verschiedenen Ebenen, kein gemeinsamer State, keine dokumentierte Konflikt-Konstellation.
  • Effektiv ein logisches OR: eine Domain wird geblockt, wenn einer der beiden Layer sie fängt. Keine Reihenfolge-Abhängigkeit.
  • Beide brauchen ohnehin schon Entitlements, die die App hat (url-filter-provider + family-controls, siehe with-rebreak-protection-ios.js Z. 6067). Layer 2 fügt kein neues Entitlement hinzu.
  • Bewertung: Koexistenz ist der unproblematischste Punkt. Technisch sauber kombinierbar.

6. Kann der User NEURLFilter „offtogglen"? — Die Kernfrage, ehrlich beantwortet

Das ist die Annahme, auf der „Fallback" steht. Nüchterner Befund:

  • NEURLFilterManager: isEnabled (Bool, App-gesteuert) und shouldFailClosed (Bool — bei true wird Traffic geblockt, wenn der Filter nicht erreichbar ist; das Repo setzt shouldFailClosed = true, RebreakProtectionModule.swift Z. 110). Beides setzt die App, nicht der User.
  • System-Toggle für den User? NetworkExtension-Filter erscheinen üblicherweise unter Einstellungen → Allgemein → VPN & Geräteverwaltung bzw. als Filter-Eintrag, den der User abschalten/löschen kann. Genau dieses Verhalten ist im Repo-Code bereits sichtbar: resetUrlFilter existiert nur, weil der User „Nicht erlauben" tippen kann und iOS den Denied-State cached (protection.ts Z. 146160). Hypothese, gut gestützt, aber nicht per Apple-Doc-Zitat zu iOS 26 final belegt: Ja, der User kann NEURLFilter abschalten/ablehnen — entweder beim System-Permission-Dialog oder nachträglich in den Einstellungen.
  • ABER — das ist der Punkt: Wenn der User NEURLFilter abschaltet, ohne FC anzufassen, fängt Layer 2 das tatsächlich ab → das ist der einzige saubere Gewinn-Fall.
  • Das größere Loch: Wenn der User stattdessen in Bildschirmzeit die FC-Authorization widerruft (oder Bildschirmzeit ganz deaktiviert), fallen denyAppRemoval und Layer 2 gleichzeitig weg — lautlos, ohne App-Callback (siehe §4). Layer 2 schützt also nicht gegen den motiviertesten Bypass-Pfad eines spielsuchtgetriebenen Nutzers, sondern nur gegen die halbherzige Variante „NEURLFilter aus, FC vergessen".
  • Fazit: Das Fallback-Szenario kann real eintreten — aber es ist der schwächere von zwei Bypass-Pfaden. Layer 2 deckt das kleinere Loch und lässt das größere offen.

7. Länderabhängigkeit — sinnvoll, aber simpel halten

  • Warum überhaupt pro Land? Gambling-Märkte sind national stark segmentiert (DE: Tipico, bwin; UK: bet365, William Hill, Sky Bet; FR: Winamax, PMU/Betclic; etc.). Eine globale 50er-Liste verschwendet Slots an Domains, die im Land des Nutzers irrelevant sind. Bei nur 50 Slots ist Kuratierung pro Land der Hebel, der den Cap erträglich macht.
  • Landbestimmung — Optionen, geordnet nach Eignung:
    1. Device-Region (Locale.current.region / NSLocale.countryCode) — lokal, kein Netz, datensparsam. Empfehlung. Schwäche: Region ≠ Aufenthaltsort.
    2. User-Profil — das Repo hat bereits DiGA-Demographie (MEMORY: birth_year/profession/... user-initiiert). Ein optionales „Land"-Feld wäre DSGVO-konform und am genauesten. Aber: zusätzliche UX, und Demographie ist strikt user-initiated.
    3. IP-Geo — am genausten für „wo bin ich", aber Netzabhängig + datenschutzkritisch (Glücksspiel-Stigma, DiGA). Nicht empfohlen.
  • Empfehlung: Device-Region als Default, optionales Profil-Override. Kein IP-Geo.
  • Datenquelle der Liste: Da der Inhalt sich selten ändert, eignet sich eine statische, mit der App gebundelte JSON (country → [top50 domains]) — kein Backend-Roundtrip, funktioniert offline, kein neuer Endpoint. Alternative: bestehender Backend-Endpoint /api/url-filter/... um ein top50?country=DE-Feld erweitern, falls schnellere Updates ohne App-Release gewünscht sind. Für ~50 stabile Domains ist das Backend-Overkill.

Bewertung — ehrlich, auch kritisch

Mehrwert für Rebreak

  • Was Layer 2 abfängt, das Layer 1 nicht abdeckt: ausschließlich den Fall „User hat NEURLFilter abgeschaltet/abgelehnt, FC aber noch aktiv". In diesem (und nur diesem) Fenster blockt webContent weiterhin die Top-50.
  • Ist das der echte Gewinn? Eher marginal. Begründung:
    • Es deckt nur 50 von 208.000 Domains ab — ein spielsuchtgetriebener Nutzer findet trivial eine Casino-Domain außerhalb der Top-50.
    • Es schützt nicht gegen den motiviertesten Bypass (FC-Widerruf, §4/§6) — dann ist Layer 2 mit weg.
    • Layer 1 (NEURLFilter) ist bereits shouldFailClosed = true + es gibt Backend-Bypass-Detection (recoveringFromBypass-Phase, /api/protection/state, mark-active). Das Produkt hat also schon einen Mechanismus, der „NEURLFilter aus" erkennt und den User zur Re-Aktivierung drängt. Layer 2 dupliziert teilweise diesen Schutzgedanken, nur schwächer.
  • Honest-consultant-Fazit: Layer 2 als „50er-Always-On-Fallback" verkauft ein Sicherheitsversprechen, das es nicht halten kann. Es ist „Defense in Depth light" — nett, aber kein echter zweiter Sicherheitsgurt. Wer es einbaut, sollte es intern und im UI als „zusätzliche Härtung der bekanntesten Anbieter" framen, niemals als „Schutz bleibt, wenn Layer 1 aus ist".

Verbesserungsvorschläge / Alternativen

  1. Gambling-Kategorie statt 50er-Liste: auf iOS nicht möglich (§1). Entfällt.
  2. .auto mitnehmen — kostenlos: Statt .specific(top50).auto([...top50...]). .auto blockt zusätzlich Adult Content systemseitig gratis mit. Bei Spielsucht oft Begleit-Trigger; minimaler Mehraufwand, spürbarer Härtungs-Effekt. Trade-off: deaktiviert Safari-Private-Browsing (bei .specific aber ohnehin auch der Fall — jede Policy ≠ .none tut das).
  3. Statische Bundle-Liste > Backend-Endpoint (§7). Datensparsam, offline-fähig, kein neuer Server-Code.
  4. Das eigentliche Loch zuerst schließen: Der höhere Hebel ist FC-Widerruf-Erkennung. AuthorizationCenter.shared.authorizationStatus bei jedem App-Foreground prüfen → wenn nicht mehr .approved → aggressiver Nudge + Backend-Flag (analog recoveringFromBypass). Das adressiert §4/§6 direkt und ist mehr wert als Layer 2.
  5. Risiken:
    • Falsches Sicherheitsgefühl beim User („ich bin geschützt") — bei einem DiGA-/Suchthilfe-Produkt ein ernstes Thema. UI-Wording streng.
    • Private-Browsing-Deaktivierung in Safari als Nebeneffekt — für die Zielgruppe vermutlich erwünscht, aber dokumentieren.
    • FC-Auth-Verbrauch: Layer 2 hängt an derselben FC-Authorization wie denyAppRemoval. Kein neues Risiko, aber: ein einziger Widerruf killt beides.
  6. Aufwand grob: klein. ~0,51 Tag. Ein neuer AsyncFunction im bestehenden Swift-Modul, ein gebundeltes JSON, JS-Bridge-Methode, ein Aufruf im Aktivierungs-Flow. Kein neues Entitlement, kein Plugin-Eingriff (FC + App-Group sind schon da).

Wann es sich doch lohnt

Wenn das Team es als bewusste, klein gehaltene Zusatz-Härtung akzeptiert (nicht als Fallback-Versprechen) und parallel die FC-Widerruf-Erkennung baut — dann ist der Aufwand niedrig genug, dass „nice to have" vertretbar ist. Als alleinige Layer-2-Strategie: zu schwach.


Implementierungs-Skizze (KEIN Code — nur Plan)

Betroffene Dateien:

Datei Änderung
modules/rebreak-protection/ios/RebreakProtectionModule.swift Neuer AsyncFunction("activateWebContentFilter"): Land empfangen, Top-50 setzen via ManagedSettingsStore(named: MS_STORE_NAME).webContent.blockedByFilter = .auto(domains) bzw. .specific(domains). Setzt voraus, dass FC bereits authorisiert ist. disable (Z. 368) erweitern: webContent.blockedByFilter = .none bzw. via clearAllSettings() (deckt es schon ab — prüfen).
modules/rebreak-protection/src/RebreakProtection.types.ts Typ für die neue Bridge-Methode + ggf. webContentFilter-Layer in DeviceLayers.
modules/rebreak-protection/src/RebreakProtectionModule.ts Bridge-Deklaration.
modules/rebreak-protection/src/RebreakProtectionModule.web.ts No-op-Stub.
lib/protection.ts Orchestrierung: nach activateFamilyControls() zusätzlich activateWebContentFilter({country}) aufrufen; getDeviceState/getCombinedState ggf. um Layer-Status erweitern.
assets/-Bundle (neu) gambling-top50-by-country.json ({ "DE": [...], "GB": [...], ... }). Mit der App gebundelt.
Backend Nicht nötig bei Bundle-Variante. Nur falls Server-Updates gewünscht: /api/url-filter/-Bereich um top50.json?country= ergänzen.
plugins/with-rebreak-protection-ios.js Keine Änderung — FC-Entitlement + App-Group sind bereits vorhanden.

Grobe Schritte:

  1. Top-50-Gambling-Domains pro Zielland kuratieren (DE/GB/FR zuerst — die i18n-Sprachen des Repos). Quelle: vorhandene 208k-PIR-Liste nach Land/Traffic-Rang filtern (backend/scripts/generate-pir-input.ts ist verwandter Kontext).
  2. Land via Locale.current.region bestimmen (optional Profil-Override); JSON-Lookup.
  3. WebDomain-Set bauen, blockedByFilter setzen (.auto empfohlen — Adult-Content gratis mit). FC-Auth-Status vorher prüfen.
  4. Disable-Pfad: sicherstellen, dass clearAllSettings() auch webContent zurücksetzt (vermutlich ja — verifizieren), sonst explizit .none setzen.
  5. Empirisch testen auf iPhone (iOS 26, kein Simulator — MEMORY-Regel): (a) blockt es eine Top-50-Domain in Safari? (b) auch in Chrome iOS? (c) verträgt es sich sichtbar mit aktivem NEURLFilter? (d) was passiert bei FC-Widerruf?
  6. UI-Wording festlegen — kein „Fallback"-Versprechen.

Offene Entscheidungen für den User

  1. Layer 2 überhaupt bauen? Ehrliche Empfehlung: nur als bewusst kleingehaltene Zusatzhärtung — und nur zusammen mit FC-Widerruf-Erkennung. Als alleiniger „Fallback" zu schwach. → Deine Entscheidung.
  2. .auto (Adult-Content gratis mit) oder .specific (nur Gambling)? .auto empfohlen, falls Adult-Content-Block für die Zielgruppe ok ist.
  3. Bundle-JSON oder Backend-Endpoint für die Top-50? Empfehlung Bundle (datensparsam, offline). Backend nur falls Updates ohne App-Release wichtig.
  4. Welche Länder zum Start? Vorschlag: DE, GB, FR (= vorhandene App-Sprachen).
  5. Landbestimmung: Device-Region (empfohlen) vs. optionales Profil-Feld?
  6. Soll stattdessen/zuerst die FC-Widerruf-Erkennung gebaut werden? Das ist nach dieser Recherche der höhere Hebel — und schließt das größere Loch, gegen das Layer 2 nicht hilft.

Quellen