5 min

Een formulier tegen spam beschermen zonder Captcha: van Akismet naar mijn eigen antispam

In november 2016 publiceerde ik op mijn WordPress-blog een bericht met de titel “Hoe stel je Akismet in?”. Ik legde uit hoe je de antispam-plug-in activeert die met WordPress meekomt, een API-sleutel ophaalt op de officiële site, en vervolgens kiest tussen ongewenste reacties meteen verwijderen of een proefperiode van veertien dagen. Ik deelde zelfs een klein filter om in functions.php te plakken en die termijn naar dertig dagen te brengen, met een zin die eigenlijk alles al samenvatte: “we zijn nooit veilig voor een vals-positief.” Tien jaar later bescherm ik geen blogreacties meer, maar de formulieren van mijn eigen site, gebouwd op Pulsar, mijn PHP-framework. En die reflex uit 2016, liever een twijfelachtig bericht bewaren dan een goed bericht kwijtraken, is de kernregel geworden van de antispam die ik uiteindelijk zelf schreef.

Wat Akismet me had geleerd

Destijds delegeerde ik alles. Akismet combineerde een woordenlijst met ongewenste termen, een set regels en een zwarte lijst, en sorteerde reacties in wachtrij of spam. Voor een blog werkte dat goed en ik raadde het zonder voorbehoud aan.

Maar delegeren heeft een prijs. Elk bericht gaat naar servers van derden voor analyse, wat op een contactformulier waar een prospect zijn naam en e-mailadres achterlaat een echt AVG-vraagstuk is geworden. En vooral: de kost van een vals-positief is van schaal veranderd. Een verloren blogreactie is jammer. Een verloren offerteaanvraag is een klant die niet terugkomt. Mijn oude site bezweek uiteindelijk onder de spam via het contactformulier, en terwijl ik de nieuwe herdacht, legde ik een vertrekvoorwaarde vast: serieus filteren, zonder de rekening ooit bij de mens te leggen.

Waarom geen zichtbare Captcha

Het gebruikelijke antwoord op spam is een Captcha. Ik heb die om drie redenen terzijde geschoven.

De frictie, om te beginnen. Elke stap die je vóór het verzenden toevoegt, doet echte bezoekers afhaken, terwijl serieuze bots die puzzels oplossen of laten oplossen door klikfarms. Men verliest mensen om machines af te remmen die niet meer worden afgeremd.

De toegankelijkheid, vervolgens. Verkeerslichten herkennen in wazige miniaturen sluit slechtziende mensen uit, en de audio-alternatieven zijn voor iedereen vervelend.

De privacy, ten slotte. De Captcha’s van de grote platformen laden scripts van derden, observeren het gedrag van de bezoeker en roepen toestemmingsvragen op die ik niet wilde beheren. Mijn site roept geen enkele externe dienst aan voor zijn formulieren: alles is zelf gehost, niets verlaat de deur.

Een Captcha straft de mens voor een probleem dat door bots is veroorzaakt. Ik wilde het omgekeerde: controles die alleen bots opmerken.

Zeven onzichtbare lagen in plaats van een muur

Geen enkele controle is op zichzelf perfect. Gestapeld houden ze het gros van de geautomatiseerde spam tegen zonder dat de bezoeker er iets van merkt. Sinds de eerste versie van dit artikel ben ik ook op één punt van gedachten veranderd: ik bleef op conceptniveau en hield de drempels verborgen, uit voorzichtigheid. Een filter die alleen standhoudt omdat zijn parameters geheim zijn, is een fragiele filter; deze houdt ook stand wanneer hij volledig beschreven is. Hier is hij dus helemaal, cijfers inbegrepen. Elke inzending doorloopt zeven lagen, in deze volgorde:

  1. het CSRF-token, opnieuw gecontroleerd bij het verzenden;
  2. de snelheidsbegrenzing per IP-adres;
  3. de ondertekende tijdval;
  4. de honeypot;
  5. de onzichtbare proof-of-work-challenge;
  6. de inhoudsscore, die nooit blokkeert;
  7. de expliciete AVG-toestemming.

De voordeur: CSRF-token en snelheidsbegrenzing

Nog voor er sprake is van spam wordt het CSRF-token dat bij het renderen van het formulier is uitgegeven, opnieuw gecontroleerd bij het verzenden. Het is een klassieke bescherming tegen vervalste verzoeken, maar ze weert al de scripts die rechtstreeks naar het eindpunt posten zonder ooit de pagina te hebben geladen.

Daarna volgt de snelheidsbegrenzing per IP-adres: tien pogingen per uur en per formulier, binnen een vast venster, geteld in de bestandscache die de PHP-workers delen. Daarboven antwoordt de server met een 429 en een boodschap in de taal van de bezoeker. Het is de enige laag die frontaal weigert: een salvo lijkt op geen enkele mens, hoe gehaast ook.

De stille vallen: tijdval en honeypot

De tijdval. Bij het renderen van het formulier geeft de server een verborgen veld uit met een tijdstempel, ondertekend met HMAC. Een mens doet seconden, vaak minuten, over het lezen en invullen van een formulier; een inzending die in minder dan ongeveer drie seconden terugkomt, is een bot. De site antwoordt dan “bericht verzonden” zonder iets te versturen. En is de handtekening ongeldig of afwezig, dan laat de laag de inzending door: fail-open, nooit een vals-positief. Ze blokkeert alleen bij een positief bewijs van automatisering, de andere lagen dekken de rest af.

De honeypot. Een verborgen veld, onzichtbaar voor mensen, dat bots invullen omdat ze alles invullen. Is het ingevuld, dan volgt dezelfde behandeling: stille acceptatie, niets verstuurd. De bot weet niet dat hij betrapt is, dus hij leert niets.

De naam, “ref_code”, betekent niets, en dat is bewust. Chrome en wachtwoordmanagers negeren autocomplete="off" grotendeels en leiden het type van een veld af uit het label. Een val met de naam “bedrijfswebsite” wordt uiteindelijk ingevuld door de browser van een echte bezoeker, wiens aanvraag dan in de prullenbak verdwijnt zonder dat iemand het merkt. Ja, ik publiceer de veldnaam: een spammer die dit artikel leest om hem te omzeilen, botst op de zes andere lagen.

De onzichtbare challenge, een eigen Turnstile

Bleef over: de goed uitgeruste bot die JavaScript uitvoert en netjes invult. Voor die is er een zelf gehoste proof-of-work-challenge, mijn eigen tegenhanger van Turnstile: de server geeft een ondertekend token voor eenmalig gebruik uit, en de browser lost op de achtergrond een kleine berekening van ongeveer zestien bits moeilijkheid op terwijl de bezoeker zijn bericht schrijft. Geen klik, geen vakje, geen foto’s van zebrapaden. Onmerkbaar voor een mens, duur voor een bot die massaal post, zonder API-sleutel of dienst van derden. En voert de browser geen JavaScript uit, dan is er een terugvaloptie: het ontbrekende antwoord blokkeert het verzenden niet, de lagen aan serverkant nemen het over. Graceful degradation geldt ook voor de beveiliging.

De regel van nul verloren leads

Zesde laag, de inhoudsscore: linkdichtheid, tekstkwaliteit, detectie van duplicaten. Elk signaal telt punten bij een score op, en hier wijkt de filosofie af van klassieke filters: die score wijst nooit een inzending af. Stapelen de signalen zich op, dan wordt het bericht toch bezorgd, gewoon gemarkeerd voor handmatige controle in mijn inbox.

De pipeline onderscheidt dus twee families van controles. Positieve bewijzen van automatisering, zoals de honeypot, de tijdval of een salvo, wijzen af. Inhoudssignalen scoren alleen. Een echte klant die drie links naar zijn bestaande site plakt, of twee droge regels vanaf zijn telefoon schrijft, komt erdoor. Ik lees liever twee spamberichten dan dat ik een klant verlies. Dat is precies de logica van mijn dertig dagen Akismet-proefperiode in 2016, tot het einde doorgetrokken: twijfel valt altijd uit in het voordeel van het bericht.

De zevende laag heeft niets algoritmisch: een expliciet AVG-toestemmingsvakje, het enige deel van deze hele pipeline dat de bezoeker echt ziet. Na het schrappen van scripts van derden, juist omwille van de privacy, moest het formulier zelf onberispelijk zijn.

Het eerlijke scorebord

Een antispam-pipeline beoordeelt u op bewijzen, niet op beloften. Dit zijn de vier scenario’s die ik in reële omstandigheden heb geverifieerd, zonder er één te verzwijgen:

  • een echte klant vult het formulier in: het bericht wordt bezorgd;
  • een eenvoudige bot vult alles in, honeypot inbegrepen: stil geaccepteerd, nooit verstuurd;
  • een snelle bot verzendt in minder dan drie seconden: de tijdval vangt hem, stil geaccepteerd, nooit verstuurd;
  • een salvo vanaf hetzelfde adres: 429 na de tiende poging binnen het uur.

Wat het in de praktijk oplevert

Op de contactpagina en op de offertecalculator ziet de bezoeker niets van deze mechaniek. Geen vakje “ik ben geen robot”, geen puzzel, geen toestemmingsbanner voor een script van derden. Het formulier vult zich in en verzendt, klaar. Het is dezelfde zorg die ik in al mijn werk als webontwikkelaar in België steek: robuustheid mag nooit worden betaald met frictie voor de gebruiker.

Bezwijkt uw eigen formulier onder spam, dan past mijn suggestie in één zin: stapel onzichtbare controles voordat u een Captcha toevoegt die uw bezoekers wegjaagt, en houd de afwijzing voor bewijzen van automatisering. In 2016 sloot ik mijn bericht af met een uitnodiging om te reageren en te delen. In 2026 is de eerlijke versie eenvoudiger: als het onderwerp u aanspreekt, schrijf me dan via dat befaamde formulier. Het houdt stand.

Lenny Obez

Softwareontwikkelaar in Hoei. Ik bouw platformen die overeind blijven op de dagen die er echt toe doen.

Samen werken?