AntiDetect
Funktionen Technologie Preise Für wen
Doks Blog FAQ
Sprache
EnglishItalianoDeutschEspañolРусский
Download

Zehn Wege, auf denen dich das DevTools-Protokoll verrät

Jeder verbreitete Automatisierungs-Stack steuert Chrome über das DevTools-Protokoll. CDP wurde zum Debuggen gebaut, nicht zum Verstecken — und es hinterlässt an einem Dutzend Stellen Spuren. Hier steht, wo, und warum ein JavaScript-Patch es meist schlimmer macht.

CDP war nie darauf angelegt, leise zu sein

Puppeteer, Playwright, Selenium 4 und jeder darauf aufbauende „Stealth“-Wrapper teilen dieselbe Grundlage: das Chrome DevTools Protocol. Es ist derselbe Kanal, den dein Browser öffnet, wenn du F12 drückst. Chrome stellt es bereit, damit ein Debugger die Seite inspizieren und steuern kann — und es wurde mit genau einem Nicht-Ziel entworfen: zu verbergen, dass ein Debugger angehängt ist.

Das ist die Wurzel des Problems. Die folgenden Signale sind keine Fehler in Puppeteer. Die meisten sind CDP, das sich korrekt verhält — auf eine Weise, wie es ein von Menschen bedienter Browser nie tut. Ein Erkennungsanbieter muss nichts kaputtmachen, um sie zu finden; er muss nur Werte lesen, die die Plattform kostenlos herausgibt.

Es folgt die Liste mit Stand 2026, grob danach sortiert, wie billig jeder Punkt zu prüfen ist.

navigator.webdriver, und warum ein JS-Patch schlimmer ist

Das älteste Signal. navigator.webdriver liefert true, sobald Chrome mit aktivierter Automatisierung gestartet wurde. Jedes Tutorial rät, es zu überschreiben:

Object.defineProperty(navigator, 'webdriver', { get: () => false })

Das ist keine Lösung. Es ist ein zweites, lauteres Signal. In einem unveränderten Browser ist webdriver ein Accessor auf Navigator.prototype, keine eigene Eigenschaft der Instanz. Nach dem obigen Schnipsel sind drei Dinge wahr, die sonst nirgends wahr sind:

Erkennungsskripte laufen die toString-Kette seit Jahren ab. Ein Browser, der das ehrliche true liefert, ist ein Browser mit Automatisierung. Ein Browser, der ein gefälschtes false liefert, ist ein Browser mit Automatisierung, der das zu verbergen versucht — und das ist für die meisten Risiko-Engines das Schlimmere von beidem.

Die einzige Variante, die einer Prüfung standhält, ist die, bei der der C++-Getter selbst false zurückgibt: keine eigene Eigenschaft, keine Arrow Function und nichts, was in der Prototypenkette fehl am Platz wäre.

Runtime.enable: das Leck, das allen den Stack umgeschrieben hat

Um JavaScript in einer Seite auszuführen, rufen die meisten Bibliotheken Runtime.enable auf. Dieser eine Befehl hat Nebenwirkungen, die bis in die Seite reichen: Er beginnt, Runtime.executionContextCreated-Benachrichtigungen zu senden, und ändert, wie Konsolenargumente serialisiert werden.

Der klassische Exploit passt in einen Tweet. Lege ein Objekt mit einem Getter an, übergib es an console.debug und schau, ob der Getter feuert. Mit angehängtem Debugger und aufgerufenem Runtime.enable serialisiert das Protokoll das Objekt für den Client, und der Getter läuft. Ohne Debugger rührt es niemand an.

let leaked = false;
const probe = { get id() { leaked = true; return 1; } };
console.debug(probe);
// leaked === true  →  something is listening on CDP

Das hat das Ökosystem zu gepatchten Forks getrieben — rebrowser-puppeteer und Verwandte —, die Runtime.enable meiden und stattdessen isolierte Welten oder addBinding nutzen. Diese Patches funktionieren und sind ihr Geld wert. Sie schließen allerdings genau ein Loch von zehn.

Drei Fehler bei synthetischer Eingabe

Wenn Automatisierung eine Maus bewegt, bewegt sie keine Maus. Sie ruft Input.dispatchMouseEvent auf, und Chrome baut ein Ereignisobjekt, das dem, was die echte Eingabe-Pipeline erzeugt, sehr nahekommt — aber eben nicht ganz. Drei Unterschiede sind von einer Seite aus trivial beobachtbar.

screenX und screenY stimmen nicht

Das ist Chromium-Bug 40280325. CDP setzt screenX = clientX und screenY = clientY — Viewport-Koordinaten dort, wo Bildschirmkoordinaten hingehören. Bei einem echten Klick unterscheiden sich beide um die Fensterposition plus die Browser-Chrome: Tableiste, Adressleiste, Lesezeichen. Bei einem maximierten Fenster sind das rund 90 bis 130 Pixel vertikaler Versatz.

Ein echter Klick nahe dem Seitenanfang hat also ein screenY um die 150. Ein CDP-Klick an derselben Stelle meldet screenY = 20. Cloudflare Turnstile wertet ein niedriges screenY als Bot-Klick, und das zu Recht: Kein Mensch kann oberhalb der eigenen Browser-Chrome klicken.

UIEvent.detail ist null

Bei einem echten Klick trägt event.detail die Klickzahl — 1 für einen Einfachklick, 2 für einen Doppelklick. Über CDP ausgelöste Klickereignisse kommen mit click_count = 0 an, also ist detail gleich 0. Keine Nutzergeste erzeugt einen Klick mit detail null. Turnstile prüft das innerhalb seines Challenge-iframes.

getCoalescedEvents() ist leer

Moderne Browser bündeln hochfrequente Zeigerbewegungen und geben die Rohwerte über PointerEvent.getCoalescedEvents() heraus. Ein echtes pointermove trägt immer mindestens ein gebündeltes Ereignis: sich selbst. Ein über CDP ausgelöstes pointermove trägt ein leeres Array, weil es nie einen Hardware-Messwert zum Bündeln gab.

Nichts davon lässt sich aus JavaScript reparieren. Das Ereignisobjekt wird in C++ konstruiert, bevor irgendein Seitenskript es sieht, und die Eigenschaften sind schreibgeschützt.

Browserfenster im Querschnitt mit dem Versatz zwischen clientY und screenY
Chromium-Bug 40280325. Ein echter Klick trägt die Browser-Chrome in seiner Bildschirmkoordinate; ein CDP-Klick meldet stattdessen die Viewport-Koordinate. Turnstile liest die Differenz.

Die Strings, die Automatisierung herumliegen lässt

Zwei Familien von Zeichenketten landen dort, wo eine Seite sie lesen kann.

ChromeDriver-Marker. ChromeDriver verfolgt die Ergebnisse eigener Aufrufe, indem es Dokument-Eigenschaften mit den Präfixen cdc_, $cdc_ oder $chrome_ anlegt. Versteckt sind sie nicht. Eine dreizeilige Schleife über Object.getOwnPropertyNames(document) findet sie, und Erkennungsskripte suchen seit 2018 genau danach.

Puppeteer-Quell-URLs. Puppeteer hängt //# sourceURL=pptr:... an die Skripte an, die es auswertet, und legt eine isolierte Welt namens __puppeteer_utility_world__ an. Beides taucht in Error.stack auf, wenn eine Ausnahme die Grenze überschreitet:

try { null.f() } catch (e) { /* e.stack may contain "pptr:" */ }

Das sind die am leichtesten zu entfernenden Signale und zugleich die, die am häufigsten stehen bleiben — weil sie nur unter Bedingungen auftauchen, die Entwickler selten testen: ein nicht abgefangener Fehler in einer ausgewerteten Funktion.

Worker: der Kontext, den niemand abdeckt

Das ist die Lücke, die die ansonsten sorgfältigsten Setups erwischt.

Web Worker, Service Worker und Shared Worker bekommen jeweils einen eigenen globalen Scope mit eigenem navigator. Dieses Objekt erzeugt die Engine im Worker-Thread aus den echten Werten des Browsers. CDP-Overrides auf Seitenebene — Emulation.setUserAgentOverride, eingeschleuste Skripte, Page.addScriptToEvaluateOnNewDocument — erreichen es nicht.

Das Ergebnis ist ein Browser, der im Haupt-Thread das eine behauptet und im Worker das andere. Ein Erkennungsskript muss nur zweimal fragen:

// main thread says 8; worker says 24
new Worker(URL.createObjectURL(new Blob([
  'postMessage([navigator.hardwareConcurrency, navigator.deviceMemory, navigator.platform])'
], { type: 'text/javascript' })));

Jede Abweichung zwischen beiden ist eindeutig. Echte Browser können keine erzeugen, weil beide Scopes dieselben zugrunde liegenden Werte lesen. Über diesen Weg lecken unter anderem hardwareConcurrency, deviceMemory, platform, die vollständige Markenliste aus userAgentData und — über OffscreenCanvas — Vendor und Renderer von WebGL.

Von außen lässt sich das nicht vollständig beheben. Die Werte müssen aus der Engine kommen, das heißt: Der navigator des Workers muss aus demselben Profil gebaut werden wie der des Fensters.

Die Kleinigkeiten, die sich trotzdem summieren

Noch drei, jede billig zu prüfen und jede für sich imstande, eine Sitzung zu beenden.

Außenmaße bei angedockten DevTools

Die Differenz zwischen outerWidth und innerWidth ist die Browser-Chrome — rund 16 Pixel horizontal und 85 bis 90 vertikal in Desktop-Chrome. Dockt man die DevTools an, springt diese Lücke um Hunderte Pixel. Ein Fenster, das outerHeight - innerHeight = 400 meldet, ist ein Fenster mit geöffnetem Debugger.

Speicherkontingent

navigator.storage.estimate() liefert ein Kontingent, das sich aus dem freien Speicherplatz ableitet. Schon für sich ein starkes Hardwaresignal — und es verrät zusätzlich Container und Inkognito-Kontexte, die charakteristische runde Zahlen weit unterhalb jeder echten Festplatte melden.

Fehlende APIs für die behauptete Plattform

Sagt dein User Agent Windows Chrome 147, kann die Seite prüfen, welche APIs Windows Chrome 147 tatsächlich mitbringt. Headless-Builds hatten historisch weder chrome.runtime noch den vollständigen Berechtigungsablauf von Notification noch die Codec-Unterstützung der Headful-Builds. Eine Plattform zu behaupten, die man nicht belegen kann, ist schlimmer, als gar nichts zu behaupten.

Warum die Lösung im Quellcode liegen muss

Betrachtet man die Liste, zeigt sich ein Muster. Fast jedes Signal entsteht unterhalb von JavaScript:

Ein Content-Skript läuft nach all dem. Es kann nur überschreiben, was längst entschieden ist, und jedes Überschreiben hinterlässt die oben beschriebenen Spuren: eigene Eigenschaften, wo Prototypen hingehören, Arrow Functions, wo nativer Code hingehört, Widersprüche zwischen Kontexten.

Das ist das ehrliche Argument für einen veränderten Chromium-Build — und es lohnt, auch seine Grenzen zu nennen. Ein Patch der Engine löst die Schicht unterhalb von JavaScript. Er löst nicht die TLS- und HTTP/2-Fingerprints, die vollständig unterhalb des Browsers liegen, und er tut nichts fürs Verhalten: Eine Sitzung, die mit unmenschlicher Regelmäßigkeit klickt, wird markiert, so sauber ihre Eigenschaften auch sein mögen.

Das eigene Setup testen

Glaub keinem Anbieter auf sein Wort, uns eingeschlossen. Die Prüfungen sind öffentlich und an einem Nachmittag durchführbar:

Lass sie gegen das laufen, was du heute benutzt, bevor du irgendetwas änderst. Ein Werkzeug, das neun dieser Prüfungen besteht und an der zehnten scheitert, ist nicht zu neun Zehnteln am Ziel — eine einzige eindeutige Abweichung reicht.

In der Engine behoben, nicht in einem Content-Skript

P8 liefert einen Chromium-Build aus, in dem sich all das so verhält wie in einem gewöhnlichen Browser — weil der C++-Code geändert und nicht umhüllt wurde.

Kostenlos starten
Alle Artikel Weiter: Browser-Fingerprinting erklärt