Bots herkennen zonder CAPTCHA: het geheim van de TLS-vingerafdruk
Hoe je nep-bezoekers van je website kunt tegenhouden zonder ook maar één irritant 'klik alle stoplichten aan'-vakje. Een heldere uitleg over een slimme, bijna onzichtbare truc.
Je kent ze wel: die vervelende vakjes waarin je moet aanvinken dat je 'geen robot' bent, of alle plaatjes met een stoplicht moet aanklikken. Dat heet een CAPTCHA. Ze zijn er om automatische programma's ("bots") tegen te houden die websites leeglepelen, accounts aanmaken of formulieren spammen.
Het vervelende: CAPTCHA's zijn irritant voor echte bezoekers, en slimme bots trappen er toch vaak doorheen. In deze post leg ik een veel elegantere aanpak uit, eentje die de bezoeker niet eens merkt. Ik doe dat in gewone taal, zonder dat je er technische kennis voor nodig hebt. Verderop zit een stukje met echte code voor wie het naadje van de kous wil, maar dat mag je gerust overslaan.
Elk apparaat heeft een soort handtekening
Stel je voor dat elke bezoeker die naar je website komt, eerst even een handje schudt met de server. Dat gebeurt echt. Het heet een handshake, en het is het moment waarop je browser en de website afspreken hoe ze veilig met elkaar praten (dat is het slotje in je adresbalk).
Het grappige is: elke soort software geeft dat handje net even anders. Chrome doet het op zijn eigen manier. Firefox weer anders. En een zelfgeschreven botprogramma? Dat schudt weer op een compleet eigen, herkenbare manier.
Het is een beetje als een handtekening, of de manier waarop iemand zijn handtekening zet. Je hoeft het gezicht niet te zien om te weten dat het niet dezelfde persoon is.
Deze "handtekening van de handshake" heeft een naam: JA3. Het is niets meer dan een korte code die precies vastlegt hóe een bezoeker dat handje schudt. Twee echte Chrome-browsers geven dezelfde code. Een botje dat met eigen software werkt, geeft een compleet andere.
Waar de meeste beveiliging steekjes laat vallen
Veel websites controleren die handtekening al, maar alleen bij de eerste klik, als je de pagina opent. Slimme botmakers weten dat, en hebben trucjes ontwikkeld om zich bij die eerste klik voor te doen als een echte Chrome-browser. Ze vermommen dus hun handtekening. Tot zover niets nieuws.
Maar hier komt het slimme deel. Veel moderne websites (denk aan chats, live-updates, notificaties) openen ná het laden van de pagina nóg een verbinding op de achtergrond. Zo'n permanente lijn heet een WebSocket. Stel je een telefoonlijn voor die openblijft zodat de site je live dingen kan doorsturen.
En juist bij het openen van díe tweede lijn vergeten bijna alle bots hun vermomming. Ze schudden dat tweede handje weer met hun eigen, echte software, want de vermommingstrucs zijn gebouwd voor de gewone verbinding, niet voor deze achtergrondlijn.
De truc in één zin
De aanpak komt hierop neer:
- Als iemand je pagina opent, noteer je stiekem zijn handtekening (JA3) en koppel je die aan zijn bezoek.
- Als diezelfde bezoeker daarna de achtergrondlijn (WebSocket) opent, controleer je de handtekening opnieuw.
- Kloppen de twee handtekeningen? Welkom. Verschillen ze? Deur dicht.
Een echte browser gebruikt voor allebei dezelfde software, dus de handtekeningen zijn identiek. Automatisch. De bezoeker merkt er niets van: geen vakje, geen stoplichten, niks.
Een bot die de eerste keer vermomd was maar bij de tweede lijn zijn ware aard laat zien, valt meteen door de mand. De twee handtekeningen matchen niet, en de verbinding wordt geweigerd.
Anders gezegd: je mag best aan de deur kloppen, maar je moet naar binnen kunnen met dezelfde hand waarmee je je hebt aangemeld.
Werkt dit echt?
Ja. Om het te bewijzen heb ik een klein testopstellinkje gebouwd met drie scenario's:
Scenario Wat er gebeurt Uitkomst Echte bezoeker Zelfde software voor pagina én achtergrondlijn Toegelaten Gestolen sessie Bot pikt de toegang, maar opent de lijn met eigen software Geweigerd Consistente bot Bot gebruikt overal dezelfde (bot-)software ToegelatenDe eerste twee zijn precies wat je wil: de echte bezoeker komt binnen, de stiekeme bot wordt eruit gegooid, zonder één CAPTCHA.
Het derde scenario is er voor de eerlijkheid. Een bot die overal netjes dezelfde software gebruikt, komt met déze ene truc wel binnen. Deze check bewijst namelijk dat dezelfde bezoeker aan beide kanten van de lijn zit, niet dat het per se een mens is.
Voor de nieuwsgierigen: hoe ziet dat er in code uit?
Geen zin in code? Sla dit blok gerust over, de rest leest gewoon door. Voor wie wél wil kijken: hieronder de kern van mijn proof-of-concept, in gewoon Python zonder externe pakketten.
Eerst: zo'n "handtekening" is echt gewoon een tekst. Je plakt een paar eigenschappen van de handshake achter elkaar en haalt daar een korte code (een MD5-hash) uit. Twee identieke browsers geven letterlijk dezelfde string:
771,4865-4867-4866-49195-49199-...-156-157-47-53,0-5-10-11-13-...-65281,4588-29-23-24-25-256-257,0
│ │ │
versie lijst van cipher-suites lijst van extensies ...
→ MD5: 346e39f02e92c9e5e1bafdd34ec3bce5
Het lastige is dat je die handtekening moet aflezen voordat de beveiligde tunnel dichtklapt. De standaardgereedschappen geven je de verbinding pas ná de handshake, en dan zijn die bytes al weg. De truc is om heel even mee te gluren naar het allereerste bericht op de lijn, zonder het te "opeten", zodat de normale beveiliging er daarna gewoon overheen kan:
def peek_client_hello(sock, timeout=5.0):
"""Lees het eerste TLS-bericht met MSG_PEEK: bekijken, niet opeten."""
sock.settimeout(timeout)
while True:
head = sock.recv(5, socket.MSG_PEEK) # gluren, cursor blijft staan
if len(head) < 5:
continue
record_len = struct.unpack("!H", head[3:5])[0]
need = 5 + record_len
buf = sock.recv(need, socket.MSG_PEEK)
if len(buf) >= need:
return buf[:need] # de ruwe handshake, intact
En dan het hart van de hele truc. Op de gewone pagina geef je de bezoeker een sessie en onthoud je bij welke handtekening die hoort. Zodra dezelfde bezoeker de achtergrondlijn opent, vergelijk je simpelweg de twee:
# Op de pagina: sessie koppelen aan de handtekening van dit bezoek.
self.sessions[session_id] = ja3
# Op de WebSocket: dezelfde handtekening opnieuw uitlezen en vergelijken.
if ja3 != self.sessions.get(session_id):
send_http(conn, "403 Forbidden", "tls fingerprint mismatch")
return # andere hand dan waarmee je je aanmeldde → deur dicht
Dat is het. Geen kunstmatige intelligentie, geen zwarte doos: drie regels vergelijken die een bot die zijn vermomming laat vallen er meteen uit filteren.
Eén slot is nooit genoeg
Daarom is deze truc bedoeld als één laag in een groter geheel. Goede beveiliging werkt als een ui: laagje op laagje. Andere lagen die je erbovenop kunt leggen:
- Een lijst van bekende, echte browsers. Accepteer alleen handtekeningen die kloppen met wat echte browsers op dit moment gebruiken.
- Vergelijk het verhaal met de handtekening. Zegt een bezoeker dat hij Chrome is, maar past zijn handtekening bij iets heel anders? Verdacht.
- Een kleine rekenopdracht op de achtergrond. Onmerkbaar voor één bezoeker, maar te duur voor iemand die duizenden nepbezoeken tegelijk wil doen.
- Controleren of de hele pagina echt geladen is, plaatjes, opmaak en al. Bots slaan dat vaak over.
Geen enkele laag is op zichzelf onbreekbaar. Maar samen maken ze het zó vervelend om binnen te komen, dat de meeste kwaadwillenden simpelweg afhaken en het bij iemand anders proberen. En dat is precies het doel.
Kort samengevat
- Elk stukje software heeft een herkenbare "handtekening" bij het opzetten van een veilige verbinding.
- De meeste beveiliging controleert die maar één keer; bots vermommen zich daar handig omheen.
- Door de handtekening een tweede keer te controleren, op een achtergrondlijn die bots vaak vergeten te vermommen, vang je een hoop bots, volledig onzichtbaar voor echte bezoekers.
- Het is geen wondermiddel, maar als één laag in een goede beveiliging is het verrassend effectief én irritatievrij.
Dit is een defensieve techniek: bedoeld om je eigen website, formulieren of diensten te beschermen. Het is getest in een afgesloten testomgeving, niet gericht tegen wie dan ook.
Tags
TLS-fingerprinting, JA3, botdetectie, WebSocket-beveiliging, anti-scraping, CAPTCHA-alternatief, websitebeveiliging, Python, cybersecurity