Du klickst auf einen Link zu einer Lokalzeitung, und die Überschrift lädt, dann das Bild, dann die ersten drei Absätze, und du hast schon angefangen zu lesen, bevor sonst irgendwas passiert. Dann wird die Seite leer. Oder sie verwandelt sich in einen Haufen ungestylter Links, das Logo über die ganze Bildschirmbreite gezogen. Oder ein Kasten schiebt sich hoch und will, dass du entweder Tracking akzeptierst oder 2,99 £ im Monat zahlst, ohne dritten Knopf.

Der Artikel war auf deinem Rechner. Der Server des Verlags hatte ihn schon komplett geschickt, Text und Stylesheets, und dein Browser hatte ihn schon gezeichnet. Weggenommen hat ihn Code, den der Verlag danach laufen lassen wollte, auf deinem Computer, eingekauft bei einem kommerziellen Anbieter, dessen Produkt darin besteht, zurückzunehmen, was gerade geliefert wurde.

Ich betreibe zu Hause ein Pi-hole1, das Werbe- und Tracking-Hosts für jedes Gerät im Netz blockiert. Auf einer wachsenden Liste britischer Nachrichtenseiten reichte das, damit die Seite zerstört wurde. Also habe ich am 22. September 2026 eine Chrome-Erweiterung gebaut, die das verhindert, sie heißt Keep The Page. Der Code liegt auf GitHub unter damo2929/browserplugin, MIT-lizenziert, und hier steht, was sie tut, was ich beim Bauen in diesen Seiten gefunden habe und welche Fixes es schlimmer machten, bevor der richtige auftauchte.

Sie ist kein Paywall-Umgeher. Wenn der Server den Artikel nie geschickt hat, zaubert hier nichts ihn herbei. Die Times blieb zu, und das ist richtig so. Behalten wird, was dir schon geschickt wurde.

Der Artikel kommt an, dann ist er weg

Von außen funktionieren alle gleich. Das HTML kommt vollständig an, und dann entscheidet ein Script, meist von einem Host des Anbieters statt der Zeitung geladen, dass du es nicht wert bist, und entfernt es.

Was es entfernt, und wie, hängt davon ab, welchen Anbieter der Verlag gekauft hat. Diese vier sind mir begegnet, jeder beim Bauen von einer echten Seite abgelesen:

VerlagWas ankommtWas die Seite dann mit sich selbst macht
Newsquest (269 Titel)2der ganze Artikelbaut eine Wand, führt eine eval-Payload aus, öffnet einen confirm()-Dialog
notebookcheck.netder ganze Artikellöscht <body> nach etwa 7 Sekunden, dann Dialoge, dann eine Reload-Schleife
National World (The Scotsman, Yorkshire Post)3der Artikel plus etwa 68KB eigenes CSSlöscht alle 100ms jedes <link> und <style>, für immer
Reach plc (Mirror, Daily Record, Manchester Evening News, Liverpool Echo und andere)4der ganze Artikeldeckt ihn mit „Tracking akzeptieren oder 2,99 £/Monat zahlen“ zu

Die 269 stammen nicht aus einer Pressemitteilung, die spricht von „mehr als 200 Marken“. Es ist die Zahl der Domains auf Newsquests eigenen TLS-Zertifikaten, die Liste, die die Erweiterung brauchte, um zu wissen, wo sie laufen soll.

Der Fall National World sollte Verlage am meisten beunruhigen, weil er die Seite aus Gründen kaputt macht, die mit Werbung nichts zu tun haben. Der Stylesheet-Stripper hat einen Partner, der das CSS vom Host des Anbieters zurückholt. Mein Pi-hole blockiert diesen Host. Also lief das Entfernen, das Zurückholen kam nie, und das eigene Layout der Zeitung hing dauerhaft davon ab, dass ein Werbeanbieter erreichbar ist. Das ist keine Wand. Das ist ein Verlag, der das Aussehen seiner eigenen Website einem Dritten überlässt und es nicht merkt.

Darf ich fragen, warum das Stylesheet einer Zeitung auf den Server einer Werbefirma warten muss, bevor es auf der Seite bleiben darf? Für den Leser gibt es dafür keinen Grund. Keinen.

Warum ich es Malware nenne

Ich benutze das Wort mit Absicht, als Beschreibung dessen, was der Code tut, und nicht als juristisches Urteil über irgendwen. Die Anti-Adblock-Payloads erfüllen die gewöhnliche Bedeutung in vier Punkten, und jeder wurde direkt beobachtet:

KriteriumWas ich gesehen habe
Läuft ohne Einwilligung, gegen dein Interesseniemand hat darum gebeten, und seine Aufgabe ist, dir Inhalt wegzunehmen, den du schon hast
Zerstört Daten, die dir schon geliefert wurdenArtikel und CSS kommen intakt an, dann löscht Code in der Seite sie
Verschleiert gegen das Lesenpermutierte String-Tabellen wie o[293 * (r + 450) % e], ausgeführt über eval
Umgeht Blockieren und bestraft EingriffeCNAME-Cloaking gegen DNS-Blocklisten, Anti-Tamper-Prüfungen, die zu einem Dialog oder einer Reload-Schleife eskalieren

Die Verschleierung ist keine Minifizierung. Minifizierter Code ist klein. Das hier ist Code, der so angeordnet ist, dass du nicht mit grep finden kannst, was er tut, und die Payload läuft dann durch eval, damit nichts auf der Platte zu dem passt, was ausgeführt wird.

Das Cloaking ist eine bekannte Technik mit eigener Forschungsliteratur5. Der Loader wird von etwas geholt, das wie eine Subdomain der Zeitung aussieht, und dieser Name löst zum Anbieter auf:

a02342.<publisher-domain>   ->  cdn-52-x.privacy-mgmt.com      (Sourcepoint)
fb.html-load.com            ->  adshield-fallback-dev-wskxz.b-cdn.net

Die Subdomain ist pro Titel zufällig. Eine Blockliste, die Hosts benennt, kommt da nicht hinterher, und genau darum geht es.

Und der Code behandelt jeden Eingriff als Schuldbeweis. Die entschlüsselten Fehlertexte in der Payload, die Pflegende von Filterlisten Ad-Shield zuschreiben6, lauten wörtlich Vital API blocked und Vital API blocked (eval). Sourcepoints Loader schreibt ein Attribut, liest es sofort zurück und wirft einen Fehler, wenn sich der Wert geändert hat:

z.call(O,'src',G), O[x]('src') !== G && throw E
… catch (W) { try { await l(W) } catch (x) { o(W) } }   // o() raises the dialog

Sourcepoint hat das offen verkauft. In der eigenen Dokumentation stand „on average about 30% of messaged users will turn off their adblockers“7. Ich beschreibe also kein Schurken-Script, das jemand eingeschmuggelt hat. Es ist ein Produkt, vom Verlag gekauft und absichtlich eingesetzt.

Die Consent-or-Pay-Wände sind eine andere Kategorie, und die nenne ich nicht Malware. Sie zerstören nicht, was geliefert wurde, und sie verstecken sich nicht vor Blocklisten. Sie üben auf andere Weise Zwang aus, und darum geht es im nächsten Abschnitt.

1.467 Partner akzeptieren oder 2,99 £ zahlen

Die Wand auf den Reach-Titeln ist Quantcast Choice, inzwischen von InMobi betrieben8 und von cmp.inmobi.com ausgeliefert. Sie bietet zwei Möglichkeiten. Akzeptieren oder 2,99 £ im Monat zahlen. Ein kostenloses „Nein“ gibt es nicht.

Wer akzeptiert, teilt seine Daten mit 1.467 aufgeführten Partnern und bekommt ein euconsent-v2-Cookie, das 13 Monate hält. Niemand liest eine Liste von 1.467 Firmen, niemand könnte abwägen, was jede davon mit den Daten machen würde, selbst wenn er sie läse, und die Zahl allein sagt dir, was für eine Einwilligung hier verlangt wird.

Die britische DSGVO sagt, dass eine Einwilligung freiwillig sein muss, und dass man bei der Beurteilung darauf schaut, ob der Dienst von einer Einwilligung in Verarbeitung abhängig gemacht wurde, die er gar nicht braucht9. Das ICO hat Leitlinien veröffentlicht, nach denen Consent or Pay rechtmäßig sein kann10, Leitlinien, die es inzwischen als in Überprüfung bezeichnet, und der Europäische Datenschutzausschuss hat gesagt, dass es bei großen Plattformen, die nur diese zwei Optionen anbieten, in den meisten Fällen nicht rechtmäßig sein wird11. Ich werde nicht so tun, als hätte die Aufsicht es verboten. Hat sie nicht.

Also, hier lande ich, ganz offen. Ich habe mich entschieden, die Wand zu entfernen und die Einwilligung zu verweigern. Das heißt, ich lese den Artikel auf dem kostenlosen Zweig, ohne zu zahlen und ohne meine Daten an 1.467 Firmen zu geben. Das ist eine Entscheidung. Die Erweiterung sagt das auf ihrer eigenen Einstellungsseite, und ich verkleide es nicht als etwas Neutrales. Eine Einwilligung, die du nicht verweigern kannst, ist ein Preis, und ich zahle keinen Preis, der als Frage verkleidet ist.

Den Kasten zu entfernen ist das leichte Drittel. Die anderen zwei Drittel stehen in Die Einwilligungsfrage richtig beantworten, denn ein Banner, das du löschst, ohne zu antworten, kommt jedes Mal wieder.

Ein Tag, acht Commits

Das Ganze wurde an einem Tag gebaut. Erst ein paar Stunden Herumstochern in Seiten, bevor irgendwas committet wurde, dann acht Commits zwischen 20:32 und 22:50. Ich habe es mit Claude Code gebaut, und viel von der Knochenarbeit, verschleiertes Inline-Script Zeile für Zeile zu lesen, hat der Agent gemacht, während ich zugesehen habe, was die Seiten auf dem Bildschirm taten. Diese Arbeitsteilung hat gut funktioniert, und wo sie schiefging, steht weiter unten, weil sie auf eine Art schiefging, die man kennen sollte.

UhrzeitCommit
20:32erster Commit: Newsquest, notebookcheck, National World, Reach, Page Six
20:46der Anbieter hinter jedem Mechanismus, ins README geschrieben
20:5650 Links von der Google-News-Startseite, als nicht ausgesuchte Stichprobe
21:02die Consent-API beantworten, jeder Zweck abgelehnt
21:11Social-Links entschärft
22:50abschaltbare Schutzfunktionen, MSN und Bing, Ablehnen per Klick, 77 Tests

Es ist eine Manifest-V3-Erweiterung mit zwei Berechtigungen, declarativeNetRequest und storage, ohne Host-Berechtigungen und ohne eigenen Netzzugang, sie kann also nichts abrufen, keine Antwort umschreiben und mit keinem Server für dich reden. Diese Einschränkung hat das Design mehr geprägt als alles andere, weil die interessante Arbeit in der Seite passieren muss.

Zwei Welten, ein Attribut nach unten und ein Event nach obenDer Code der Seite und die Einstellungen der Erweiterung leben in verschiedenen WeltenErweiterungsspeicherchrome.storage.localSchutzfunktionenTracing-KanäleSocial-Einstellungenletzte 50 Fehlergelesen und geschriebenvon der EinstellungsseiteIsolierte Weltbridge.jssocial.jsportal.jskann chrome.storage lesenkann die Funktionen derSeite nicht anfassenWelt der Seite (MAIN)walls.jsguard.jsportal-early.jsumhüllt setTimeout, cookie,__tcfapi, window.adLightgar kein chrome.*Nach unten: ein Attribut am html-Element, nur was aus ist<html data-ktp-off="cookies,dom">Standardinstallation: nichts geschriebenNach oben: ein ktp-report-Eventdetail: ein JSON-Stringein Objekt kommt nicht zuverlässig an
Chrome führt Erweiterungs-Scripts in zwei Welten aus. Die Welt der Seite erreicht das JavaScript der Seite, hat aber keine Erweiterungs-APIs. Die isolierte Welt kann Einstellungen lesen, aber die Funktionen der Seite nicht anfassen. Alles wandert zwischen ihnen als Attribut nach unten und als Event nach oben.

Chromes MAIN-Welt teilt sich die JavaScript-Umgebung der Seite12. Ein Script dort kann setTimeout ersetzen, document.cookie umhüllen oder window.adLight definieren, bevor die Seite es tut, und genau das braucht man gegen diese Wände. In der Praxis kann es chrome.storage nicht aufrufen. Die isolierte Welt kann das, sieht aber die Funktionen der Seite nicht. Also liest eine kleine Brücke die Einstellungen und schreibt sie auf <html>, und Fehler kommen als CustomEvent zurück, dessen Detail ein JSON-String ist, weil ein Objekt diese Grenze nicht zuverlässig überquert.

Das Attribut nach unten listet nur, was du abgeschaltet hast. Eine Standardinstallation schreibt überhaupt nichts in die Seite. Das zählt, weil eine dauerhafte Markierung auf <html> genau das ist, wonach diese SDKs suchen, und weil ein Speicherfehler dadurch in Richtung Schutz der Seite ausfällt statt davon weg.

Finde das Tor, bekämpfe nicht die Wand

Der Newsquest-Fix sind zwei Zeilen Überlegung, und an ihm wurde alles andere gemessen.

Ihre ganze Wand hängt an einem Flag in der Seite:

var adLight = false;        // line 1647
if (adLight !== true) { …}  // line 2020: loader, eval payload, confirm()

adLight ist das Abonnenten-Flag für „wenig Werbung“. Ist es wahr, entsteht die Wand nie. Also definiert die Erweiterung window.adLight bei document_start als true, bevor das eigene Script der Seite läuft, mit einem Setter, der ignoriert, was hineingeschrieben wird:

Object.defineProperty(window, 'adLight', {
  configurable: false,
  enumerable: true,
  get() { return true; },
  set() { /* ignore the page's "false" */ }
});

Ein var oben in einem Script definiert eine Eigenschaft, die das globale Objekt schon hat, nicht neu. Es weist ihr nur einen Wert zu13. Also läuft das var adLight = false der Seite, landet im Setter und bewirkt nichts. Der Loader der Wand startet nie, die eval-Payload kommt nie an, und es gibt keine Anti-Tamper-Prüfung, die anschlagen könnte, weil nichts manipuliert wurde. Das Flag hat einfach Ja gesagt.

Das ist die einzige Eigenschaft in der ganzen Erweiterung, die nicht konfigurierbar ist. Sie muss es sein, um die Deklaration zu überleben. Alles andere ist configurable: true, damit keiner Seite je eine ihrer eigenen APIs für immer weggenommen wird.

Es deckt 269 Titel ab und lässt sich in den Einstellungen nicht abschalten, und die sagen auch warum: Es läuft, bevor chrome.storage antworten kann, und einmal gesetzt lässt es sich nicht rückgängig machen. Eine Checkbox wäre Dekoration.

Jeder Fix, der es schlimmer machte

Das ist der nützliche Abschnitt, weil jeder Fehler hier das Naheliegende ist, das man probiert.

Was ich probiert habeWas passiert ist
Den Host des Loaders blockierender Host ist pro Titel ein zufälliger First-Party-CNAME, und ein fehlgeschlagener Abruf ist selbst das Erkennungssignal
setAttribute bei eingefügten Scripts bewachenlöste Sourcepoints Rücklese-Prüfung aus, die genau den Dialog öffnete, den es verhindern sollte
Den confirm() mit Abbrechen beantwortenin diesem SDK heißt Abbrechen neu laden, also eine endlose Reload-Schleife
<body> bei DOMContentLoaded sichern, um es später wiederherzustellendie Wand leert schon beim Parsen, die Sicherung war also von einem leeren Body
remove() und removeChild() bewachendie Wand räumt die Seite mit einem einzigen innerHTML = '' ab, nicht Knoten für Knoten
Alles auf einmal bewachen, bei notebookcheckdie Wand eskalierte zu einem Dialog, dann zu einer Reload-Schleife, klar schlimmer als gar nichts zu tun
Den Anbieter-Host auf einen lokalen Stub umleiteneine redirect-Regel mit nur declarativeNetRequest machte das ganze Regelwerk ungültig und tötete still die Newsquest-Regel auf 269 Seiten
Die Seiten-Wächter auf jeder Seite laufen lassenpatchte globale Prototypen auf jeder Seite, die ich besuchte, auch bei meiner Bank

Die Umleitung lohnt einen zweiten Blick. Chrome gibt einer block-Regel impliziten Zugriff und will für alles darüber hinaus eine Host-Berechtigung14. Laut Dokumentation werden ungültige statische Regeln ignoriert15. Was ich gesehen habe, war schlimmer: Die ganze Datei war weg, ohne Fehler auf der Seite, und Regel 1 existierte auf 269 Seiten einfach nicht mehr. Die zwei Regeln für MSN liegen deshalb jetzt in einem eigenen Regelwerk, damit eine schlechte Änderung an einem nicht das andere mitreißt.

Die Reload-Schleife hat die andere harte Regel gelehrt. location.reload lässt sich nicht abfangen. Das Location-Objekt ist im HTML-Standard unfälschbar16, seine Methoden sind also weder beschreibbar noch konfigurierbar, und der Versuch liefert:

Object.defineProperty(location,'reload',...)
-> TypeError: Cannot redefine property: reload

Eine Wand, deren Fehlerpfad „Seite neu laden“ heißt, lässt sich nicht mehr aufhalten, sobald sie auf diesem Pfad ist. Durch nichts. Der einzige Fix ist, dafür zu sorgen, dass sie nie dort ankommt. Das ist dieselbe Lektion wie bei adLight, auf die schmerzhafte Art gelernt: Jeder Versuch, eine schon laufende Wand zu bekämpfen, machte es schlimmer, und jeder Fix, der funktionierte, hielt die Wand vom Starten ab.

Ein Timer richtete den Schaden an

notebookcheck hat kein adLight. Die Wand läuft immer. Mit eingeschaltetem Tracing zeigte die Erweiterung, was sie einplante:

dropped setTimeout(7005ms) scheduled from eval    <- the body.remove()
dropped setTimeout(1251ms) / 105ms / 0ms x8
dropped setInterval(15000ms)

Einer dieser Timer richtet den ganzen Schaden an: der nach 7 Sekunden, der <body> entfernt. Alles danach ist Reaktion, denn die leere Seite wirft einen Fehler, die Ausnahme öffnet den Dialog, der Dialog lädt die Seite neu, und das Ganze fängt mit einem frischen Satz Timer wieder von vorn an.

Also ist der Fix eine einzige Regel. Verwirf einen Timer, wenn er aus eval-Code geplant wurde, die Seite die Signatur dieses SDK trägt und der Handler eine Funktion ist. Der Stack verrät, woher ein Aufruf kam, und eval hinterlässt darin seine Spur.

before: 4 page loads, 3 confirms, reload loop
after:  1 page load,  0 confirms, alive 40,378ms, content intact
Ein Timer, und alles, was daraus folgtnotebookcheck: ein Timer richtet den Schaden an, der Rest ist ReaktionWie ausgeliefertArtikel und CSSda, gezeichnetLoader vonhtml-load.comeval-Payloadplant Timer7,0s: Timer läuftbody.remove()leere Seite wirft,confirm() geöffnetSeite lädt neuund beginnt neu4 Seitenaufrufe, 3 Dialoge, Reload-SchleifeMit der ErweiterungArtikel und CSSda, gezeichnetLoader vonhtml-load.comeval-Payloadplant Timerjeder eval-Timersofort verworfen1 Aufruf, 0 Dialoge,nach 40 Sekunden am Leben
notebookcheck ohne und mit der Erweiterung. Nichts hinter dem 7-Sekunden-Timer muss repariert werden, weil nichts davon passiert, sobald dieser Timer verworfen ist.

Zu dem Zeitpunkt steckten noch zwei andere Mechanismen im Build, eine Umleitung des Loaders auf einen Stub und ein Köder-Element, das die Schreibzugriffe der Wand auffing. Beide funktionierten, und beide behandelten Symptome dieses einen Timers, was erst klar wurde, als die Timer-Regel allein getestet wurde und sich als ausreichend herausstellte. Also flogen beide raus, und mit ihnen verschwanden eine Berechtigung und jede Host-Berechtigung. Teste immer, ob die letzte Änderung allein reicht, bevor du das Gerüst drumherum behältst.

Die Signatur ist das data-sdk-Attribut am Loader-Tag, und sie passt auf eine Form, l/<n>.<n>, statt auf eine Version. An einem Tag tauchten drei Versionen auf. Sie rastet ein und merkt sich nie ein Negativ, weil das Loader-Tag vielleicht noch nicht geparst ist, wenn die ersten Timer geplant werden, und ein gemerktes „Nein“ würde sie auf einer Seite, die die Wand trägt, für immer entschärfen.

Die Messung sagte gut. Der Bildschirm nicht.

The Scotsman und die Yorkshire Post galten als repariert, auf Grundlage einer Messung, die Text zählte. Die Seite hatte 8.808 Zeichen davon, 22 Elemente unter <body>, stabil nach 2, 8 und 16 Sekunden. Nach diesem Maßstab war sie intakt.

War sie nicht. Ich habe sie mir angesehen, und jedes Stylesheet war weg, die Links waren eine rohe Liste, das SVG-Logo füllte den Bildschirm, und es gab eine horizontale Scrollleiste. Alle Wörter waren da, und mehr konnte diese Messung nicht sehen. Ich musste auf den Bildschirm zeigen und es sagen.

Was die Messung maß, und was auf dem Bildschirm warYorkshire Post vor dem Fix: dieselbe Seite, auf zwei Arten gemessenWas die Messung maßinnerText.length8.808Kinder von body22nach 2s, 8s, 16sunverändertUrteil: intaktWas auf dem Bildschirm wardocument.styleSheets.length0Links als rohe Liste, Logo volle Breite,eine horizontale ScrollleisteUrteil: kaputtNach dem Verwerfen des Strippers beim Einplanen: 2 Stylesheets, 532 Regeln, die Seite rendert
Dieselbe Seite, auf zwei Arten gemessen. Die Textlänge sagte, die Seite sei gesund. Die Zahl der Stylesheets sagte, sie sei kaputt, und die Zahl der Stylesheets hatte recht.

Die Ursache war der Stripper aus der ersten Tabelle, und er passte nicht auf die Timer-Regel, weil er ein gewöhnliches Inline-Script ist und kein eval. Also ist sein eigener Quelltext die Signatur: ein wiederholter Job, dessen Rumpf querySelectorAll('link,style') und dann remove() aufruft. Nichts Legitimes tut das. Er wird beim Einplanen verworfen, nichts wird je entfernt, und nichts muss wiederhergestellt werden. Die Yorkshire Post ging von 0 Stylesheets auf 2, mit 532 Regeln, und sie wurde gerendert.

Um ihn zu finden, brauchte es einen Stacktrace, kein Raten. Patch Element.prototype.remove so, dass es den Stack protokolliert, sobald ein STYLE oder LINK entfernt wird, und es nannte das Inline-Script und das forEach beim ersten Versuch.

Danach kam document.styleSheets.length in jede Prüfung. Eine Seite ohne Stylesheets ist kaputt, egal wie viel Text sie hat. Messen ist schwieriger, als man denkt, und das sind die Fallen, in die der Build an dem Tag getappt ist:

FalleWas sie sagteWas stimmte
Textlänge als Render-Prüfung„intakt“ungestyltes Markup
eine Stichprobe nach dem LadenReach-Wände auf acht Titeln „nicht vorhanden“die Wand lebt etwa 600ms und war schon weggeräumt
leere Konsole nach der Navigation„der Wächter feuert nicht“Meldungen beim Laden überleben die Navigation nicht
Stichproben nach 1s und 4snotebookcheck gesundes leert nach 5 bis 8 Sekunden
cssRules über Origins hinweg gezähltESPN hatte 5 RegelnCross-Origin-Sheets werfen Fehler, also zählen sie zu niedrig

Der Fix für den 600ms-Fall ist, ab dem Laden der Seite alle 100ms abzufragen. Bei Wales Online erschien die Wand nach 425ms und war nach 1.129ms weg.

Dann wurden die Prüfungen ernsthaft durchgezogen. 52 Artikel auf 26 Domains, zwei pro Seite, so ausgesucht, dass jeder Mechanismus vorkommt: 52 von 52 mit Stylesheets, keine Wand auf dem Bildschirm übrig, keine Reload-Schleifen. Dann 50 Links direkt von der britischen Google-News-Startseite, nicht von mir ausgesucht: 45 normal gerendert, 2 waren Hosts, die mein Pi-hole absichtlich blockiert, 1 war die echte Paywall der Times, und 2 waren derselbe Artikel, zweimal gelandet, weil Google die Reihenfolge der Links bei jedem Laden neu aufbaut. Keine mit null Stylesheets. Drei National-World-Titel, die ich nie getestet hatte, tauchten in diesem Lauf mit demselben SDK auf, und alle drei wurden gerendert, und genau dafür passt man auf eine Form statt auf eine Liste von Seiten.

Es gibt 77 Unit-Tests, im Repository mit allem anderen. Auf diesem Rechner gibt es kein Node, also laufen sie auf gjs und laden das echte walls.js gegen ein Ersatz-DOM, damit die Muster aus der Produktion getestet werden. Jeder wurde geprüft, indem das, was er bewacht, kaputt gemacht und zugesehen wurde, wie er rot wird. Sie testen nur Entscheidungen. Dass sie bestehen, heißt nicht, dass eine Seite rendert, und der Scotsman ist der Grund, warum dieser Satz im README steht.

Die Einwilligungsfrage richtig beantworten

Ein Consent-Banner zu löschen lässt die Frage unbeantwortet. Das hat zwei Folgen, und die erste ist, dass das Banner bei jedem Seitenaufruf neu gebaut wird. Und ein Verlag, der seinen Inhalt zurückhält, bis die Consent-API antwortet, hängt einfach fest, während ein Anbieter ohne Antwort die Frage als nie gestellt behandeln kann. Schweigen ist keine Ablehnung.

Also beantwortet die Erweiterung sie, in dieser Reihenfolge:

Ablehnen, Nein sagen, nichts speichern, erst dann entfernenEin Consent-Banner wird beantwortet, nicht nur gelöschtDer Container eines Consent-Anbieters ist sichtbar#qc-cmp2-container #onetrust-consent-sdk sp_message_container1. Ihre Ablehnung drückenReject all, Decline, Only essential,Continue without acceptingnur ganze Beschriftung, max. 40 Zeichenpasst auf Accept: der Build scheitert2. Der API antworten: Nein__tcfapijeder Zweck, jede Funktion undjeder Anbieter abgelehnttcString "" tcloaded3. Datensatz nie speicherneuconsent-v2 addtl_consentOptanonConsent didomi_tokencookie und localStorageSchreibzugriffe verworfenBeim nächsten Tick noch sichtbar?ja: entfernen, Scrollen freigebennein: die Ablehnung gilt
Drei Antworten und ein Rückfall. Der Ablehnen-Knopf wird gedrückt, wenn es einen gibt, der Consent-API wird zu allem Nein gesagt, der Datensatz wird nie gespeichert, und nur ein Banner, das beim nächsten Tick noch steht, wird entfernt.

Zuerst drückt sie deren Ablehnen-Knopf. Nur Knöpfe, deren ganze Beschriftung eine Ablehnung ist: „Reject all“, „Decline“, „Only essential“, „Continue without accepting“ und ein paar mehr, jeweils verankert, alles über 40 Zeichen wird ignoriert. Ein Unit-Test füttert sie mit „I Accept“, „Accept All“, „Agree and close“, „Allow all“, „Got it“, „Subscribe“ und „Pay £2.99/mo“ und lässt den Build scheitern, wenn eines davon passt. Den falschen Knopf zu drücken hieße, in deinem Namen einzuwilligen, und das ist das Einzige, was dieses Projekt nie tun darf. Auf msn.com hat „Reject All“ Microsofts Banner verschwinden lassen, und es kam nach dem Neuladen nicht wieder, weil Microsoft die Ablehnung auf den eigenen Servern speichert.

Sie beantwortet die Consent-API mit Nein. Das Framework des IAB gibt jeder einwilligenden Seite eine Funktion namens __tcfapi, um zu fragen, wozu du zugestimmt hast17. Wo es eine gibt, beantwortet die Erweiterung sie mit jedem Zweck, jeder besonderen Funktion und jedem Anbieter abgelehnt, einem leeren Consent-String und eventStatus: 'tcloaded', was heißt, die Antwort ist endgültig. Sie erscheint nur, wo schon ein Consent-Framework auf der Seite ist, eine Seite, die nie gefragt hat, sieht sie also nicht.

Sie speichert den Datensatz nie. document.cookie und localStorage verwerfen still euconsent-v2, addtl_consent, OptanonConsent, didomi_token und den Rest, sodass eine Einwilligung, die du nie gegeben hast, nie geschrieben und auf der nächsten Seite nie an 1.467 Partner weitergespielt wird. Britisches Recht verlangt dafür ohnehin eine Einwilligung, bevor etwas auf deinem Gerät gespeichert wird18. Die Erweiterung setzt das Nein durch.

Jeder Name auf dieser Liste ist an beiden Enden verankert, und dahinter steckt eine Geschichte. Sourcepoints Cookies beginnen mit _sp_. Ein schlampiges Muster sp_ trifft auch Spotifys sp_dc und sp_t, also die Login-Sitzung, und hätte mich auf jeder Seite bei Spotify abgemeldet. Auch das hält ein Test fest.

Entfernen ist der Rückfall. Nur für ein Banner, das nach dem Klick noch auf dem Bildschirm ist, und nur für ein Banner, das tatsächlich angezeigt wird. Mehrere Anbieter lassen eine dauerhafte Hülle in der Seite, ob ein Banner da ist oder nicht. euronews hält einen Didomi-Host mit Höhe null bereit, und den herauszureißen macht die Seite ohne jeden Gewinn kaputt. Das Reach-Banner saß drei Ebenen unter <body> in zwei Hüllen, die selbst nicht fixiert sind, also muss die Prüfung bis zu dem Kasten hinuntergehen, der es ist.

Teilen-Knöpfe und ein Portal, das seine eigenen Einstellungen ignoriert

Zwei weitere Dinge auf diesen Seiten sind dazu da, jemand anderem als dem Leser zu dienen, und sie brauchten eine andere Behandlung als die Wände.

Teilen- und Folgen-Knöpfe. Jeder Link zu Facebook, Instagram, X, TikTok oder LinkedIn wird auf http://localhost/removeme umgeschrieben, das Original in einem Attribut geparkt, damit nichts verloren geht. Dann löscht ein zweiter Durchgang, was eindeutig Mobiliar ist (ein Icon ohne Text, „Share on X“, alles in einem Container, der sich share oder social nennt), und versteckt den Rest. Die Markierung ist da, damit ein einziger Selektor alles zeigt, was gleich verschwindet, und ein Nur-Markieren-Modus hört nach dem ersten Durchgang auf, damit du nachsehen kannst, bevor du ihr auf einer Seite vertraust.

Die naive Version macht Seiten auf drei Arten kaputt, und jede ist jetzt ein Test:

Naive VersionWas sie kaputt machtWas stattdessen passiert
a[href*="x.com"]trifft auch netflix.com, linux.com, phoenix.comden Host parsen und ganze Labels vergleichen
jeden Social-Link entfernen„Continue with Facebook“ verschwindet, und Leute sind ausgesperrtLogin-, OAuth-, Rechts- und Entwickler-Links bleiben in Ruhe
einen Link in einem Satz entfernendie Wörter gehen mitstandardmäßig verstecken, eine Option löst ihn auf und behält die Wörter

Das Letzte ist ein Kompromiss, den ich bewusst eingegangen bin. Eine BBC-Schlusszeile liest sich damit als „follow BBC Manchester on , , and .“. Ich habe mir das angesehen und trotzdem das Verstecken gewählt. Die Option, die Wörter zu behalten, ist für alle da, die das anders sehen.

MSN und Bing. Beide laufen auf denselben Web Components, etwa 160 Shadow Roots auf der Startseite. Eine einfache Abfrage des Dokuments fand 1 Link. Das Durchlaufen der Shadow Roots fand 73, darunter die Facebook- und X-Kacheln. Was sie nicht durchläuft, sieht fast nichts davon.

MSN hat durchaus eigene Inhaltseinstellungen. Die liegen auf Microsofts Servern, an eine anonyme ID gebunden, nicht in einem Cookie, also sind sie nach dem Löschen der Cookies, in einem neuen Profil und im Inkognito-Modus weg. Bei Bing kommen sie überhaupt nicht an. Und mit allen abgeschaltet und nach 11.700 Pixeln Scrollen war das hier noch im Feed:

MSNs eigener Schalter, ausNoch auf der Seite
Casual Gamesdie Spiele-Kachel
ShoppingWerbekacheln für Booking, Temu und eBay
Comments501 Kommentar-Links
Weather, Finance, Sportsweg, bis zum nächsten Cookie-Löschen

Ein Schalter namens Comments, der 501 Kommentar-Links auf der Seite lässt, ist eine Behauptung gegenüber dem Nutzer, und eine falsche. Also nimmt die Erweiterung sie selbst heraus, anhand von MSNs stabilen Komponentennamen statt der Klassennamen, die sich mit jedem Build ändern.

Eine Sache, die ich dort gelernt habe und die weit über MSN hinaus gilt: Ein Bild aus der Seite zu entfernen, hält es nicht vom Laden ab. Chrome startet den Abruf, sobald src gesetzt wird, selbst bei einem Bild, das nie in die Seite eingefügt wird19. Bis ein Content-Script eine Karte sieht, ist ihr Vorschaubild schon unterwegs. Also werden die Feed-Daten an den zwei Endpunkten blockiert, die sie ausliefern, und nie die Bild-Hosts, weil th.bing.com auch die Bing-Bildersuche ausliefert.

Und der MSN-Feed blitzt immer noch kurz auf dem Bildschirm auf, bevor er verschwindet. Vier Versuche, ihn früher zu verstecken, maßen alle als funktionierend, und keiner stoppte das Aufblitzen, was heißt, dass das, was gezeichnet wird, nicht das ist, was die Erweiterung versteckt. Die Einstellungsseite sagt das offen. Lieber sagt sie das, als ein sauberes Laden anzudeuten, das sie nicht liefert.

Was sie nicht tut

Sie wird nichtWeil
eine echte Paywall öffnenwenn der Server den Artikel zurückhält, bleibt er zurückgehalten
bei einer Seite rateneine Domain kommt erst rein, wenn ihre Wand dort gesehen wurde; zwei News-Corp-Seiten, von denen angenommen wurde, sie glichen Page Six, taten es nicht und flogen wieder raus
eine Seite ohne Signatur anfassenbei der BBC ist jeder Hook installiert und nichts wird protokolliert, kein Timer verworfen, kein Knoten angefasst
nach Hause telefonierenkeine Host-Berechtigungen, kein Netzzugang, keine Telemetrie; das Fehlerprotokoll bleibt in deinem Browser
so tun als obdas MSN-Aufblitzen ist ungelöst, und eine Ad-Shield-Variante, wp-ls/…, passt noch nicht; ihre Seite rendert einwandfrei, also ist das aufgeschrieben statt auf Verdacht repariert

Eine Seite, die dir geschickt wurde, gehört dir

Sobald ein Server deinem Browser eine Seite geschickt hat, liegt diese Kopie auf deinem Rechner. Danach Code darauf laufen zu lassen, um sie wieder wegzunehmen, ist kein Geschäftsmodell, das ich anerkenne. Es ist der alte Trick, dir etwas zu verkaufen und die Hand darauf zu behalten.

Zeitungen waren früher etwas, das man kaufte und dann besaß: jemand an der Straßenecke hatte einen Stapel, du gabst das Geld, und sie ging mit dir nach Hause und gehörte dir, zum Lesen, Falten, Verleihen oder zum Feuermachen. Niemand kam eine Stunde später vorbei, um die zweite Seite herauszuschneiden, weil du die Anzeigen übersprungen hattest. Die Web-Version hat die Zeitung umsonst hergegeben und dann den Leser verkauft. Erst an die Werbekunden, dann an die 1.467 Partner, und jetzt an einen Anbieter, dessen ganzes Produkt darin besteht zu entscheiden, ob du dich gut genug benommen hast, um zu behalten, was dir gegeben wurde.

Worüber ich nicht hinwegkomme, ist das Stylesheet. Eine Zeitung, die den Server einer Werbefirma entscheiden lässt, ob ihr eigenes Layout überlebt, hat ihre eigene Titelseite weggegeben. Sie wird es nicht gewusst haben, weil drinnen niemand den Host blockiert hat, also war der Erste, der es merkte, ein Leser mit einem Pi-hole, der auf eine Seite voller roher Links starrte und von einem Dialogfenster gesagt bekam, der Fehler liege bei ihm. Tat er nicht.

Journalismus hat einen Preis, und den zahle ich ohne Weiteres. Was ich nicht tue, ist ein Script entscheiden zu lassen, was ich schon habe. Diese Linie wird in meinem Browser gezogen, nicht in ihrem.


  1. Pi-hole documentation — „The Pi-hole® is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software.“ ↩︎

  2. Newsquest — About us — „We are the leading local news publisher in the UK with a portfolio of more than 200 brands.“ Marken sind keine Domains, daher die höhere Zahl von den Zertifikaten. ↩︎

  3. Daily Business, 18. Dezember 2024 — „National World, owner of The Scotsman and Yorkshire Post, has reached agreement on a £65.1 million takeover by Irish publisher Media Concierge.“ ↩︎

  4. Reach plc — About us, archiviert am 5. September 2026 — „120+ brands, from household names like the Mirror, Express, Daily Record and Daily Star, to local titles like MyLondon, BelfastLive and the Manchester Evening News“. ↩︎

  5. Dimova et al., „The CNAME of the Game: Large-scale Analysis of DNS-based Tracking Evasion“, PETS 2021 — CNAME-Cloaking „effectively bypasses antitracking measures that rely on fixed hostname-based block lists.“ ↩︎

  6. uBlockOrigin/uAssets Issue #30988, von den Pflegenden unter dem Label „Ad-Shield“ geführt, mit der von error-report.com ausgelieferten Meldung: „Failed to load website properly since html-load.com is blocked.“ Siehe auch Jacob Desforges, „Ad-Shield ad reinsertion“, 12. April 2026. Die Zuordnung stammt aus der Filterlisten-Community; der Anbieter legt nichts offen. ↩︎

  7. Sourcepoint — Anti-adblock FAQs, archiviert am 25. Mai 2022 — „Historical experience shows on average about 30% of messaged users will turn off their adblockers.“ Dieselbe Seite fragt, ob „a CNAME applied to a 1st-party subdomain“ das Blockieren des Erkennungs-Scripts verhindern würde. ↩︎

  8. AdExchanger, 16. August 2023 — „InMobi acquired Quantcast’s consent management platform, called Quantcast Choice“. ↩︎

  9. UK GDPR, Article 7 — „utmost account shall be taken of whether, inter alia, the performance of a contract, including the provision of a service, is conditional on consent“. Erwägungsgrund 42: Einwilligung ist nicht freiwillig, „if the data subject has no genuine or free choice“. ↩︎

  10. ICO — Consent or pay, veröffentlicht am 23. Januar 2025 — „“Consent or pay” models can be compliant with data protection law if you can demonstrate that people can freely give their consent“. Die Seite sagt inzwischen, dass die Leitlinien nach dem Data (Use and Access) Act überprüft werden. ↩︎

  11. EDPB Opinion 08/2024, angenommen am 17. April 2024 — „In most cases, it will not be possible for large online platforms to comply with the requirements for valid consent if they confront users only with a binary choice“. Sie betrifft große Online-Plattformen, nicht Regionalzeitungen. ↩︎

  12. Chrome for Developers — content_scripts manifest key — „Choosing the “MAIN” world means the script will share the execution environment with the host page’s JavaScript.“ ↩︎

  13. ECMAScript — CreateGlobalVarBinding — „If a binding already exists, it is reused and assumed to be initialized.“ ↩︎

  14. Chrome for Developers — declarativeNetRequest — „provides implicit access to allow , allowAllRequests and block rules“, und sonst „you must request host permissions before you can perform any action on a host.“ ↩︎

  15. Chrome for Developers — declarativeNetRequest — „Errors and warnings about invalid static rules are only displayed for unpacked extensions. Invalid static rules in packed extensions are ignored.“ ↩︎

  16. HTML Standard — the Location interface kennzeichnet seine Member als [LegacyUnforgeable], was in Web IDL bedeutet: „the property will be non-configurable and will exist as an own property on the object itself“. ↩︎

  17. IAB Tech Lab — TCF v2 CMP API — „Every consent manager MUST provide the following API function: __tcfapi(command, version, callback, parameter)“. ↩︎

  18. PECR, regulation 6 — „a person must not store information, or gain access to information stored, in the terminal equipment of a subscriber or user“, mit der Einwilligung als Bedingung in Schedule A1, geändert durch den Data (Use and Access) Act 2025. ↩︎

  19. HTML Standard — update the image data — läuft „whenever that element is created or has experienced relevant mutations“, auch wenn sein src gesetzt wird; im Dokument zu sein ist keine Bedingung. ↩︎