jkphl / antibot
Death to spambots! CAPTCHA-less form validation
Requires
- php: ^7.4 || ^8.0
- psr/http-message: ^1.1 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- monolog/monolog: ^2.9
- nyholm/psr7: ^1.8
- nyholm/psr7-server: ^1.1
- phpmd/phpmd: ^2.15
- phpunit/phpunit: ^9.6
- squizlabs/php_codesniffer: ^3.13
This package is auto-updated.
Last update: 2026-09-01 07:18:30 UTC
README
Death to spambots! CAPTCHA-less form validation
Antibot decides on the server whether a form submission came from a person or a machine. It never asks the visitor to prove anything: no puzzle, no image grid, no "click all the traffic lights", no proof-of-work in the background.
That is not a convenience — it is the point. Every challenge-response scheme moves the cost of the operator's spam problem onto the people the form is meant to serve, and none of them is fully accessible. Antibot keeps that cost where it belongs. Whatever the heuristics let through is a debt for the site operator to carry, not the visitor.
Development happens at code.tollwerk.net/joschi-kuphal/antibot. The GitHub repository is a mirror kept in step automatically — it is the public face and the source Packagist reads, but pull requests and issues opened against it may be answered from the other side.
How it works
Two ideas carry the whole library, and neither of them stores anything on the server.
The names of the protective fields are unguessable. Every Antibot instance derives a signature from the session, the form, an optional installation secret and the exact validator configuration:
signature = sha1(secret + session + serialize([form, validators]))
parameterPrefix = "antibot_" + signature
All hidden fields live under that prefix. An armor scraped from one form is therefore worthless in another session, in another form, or after the configuration has changed — the submitted parameters are simply not found, and the submission counts as unvalidated rather than valid.
The time is carried inside a signed token. The HMAC covers the moment the form was served, so a submission that arrives too fast, too late, or with a doctored timestamp is refused without the server having remembered anything about it.
Around those two, a chain of validators inspects the request in a fixed order — trusted addresses first, so that nothing later can overrule them:
| Position | Validator | What it looks at |
|---|---|---|
| 0 | IpWhitelistValidator |
Addresses that are always let through |
| 10 | IpBlacklistValidator |
Addresses that are never let through |
| 15 | BanValidator |
A subject (address or field value) that is currently banned |
| 20 | ParameterBlacklistValidator |
A single field value against a lookup pool |
| 30 | DuplicateValidator |
Content that has been submitted before |
| 40 | ParameterSetValidator |
Whether the submitted field names match the ones handed out |
| 50 | HoneypotValidator |
Fields no browser fills in |
| 60 | ParameterPlausibilityValidator |
Word and character counts per field |
| 61 | ParameterLinkValidator |
The number of hyperlinks in a field |
| 62 | ParameterEqualityValidator |
The same value repeated across unrelated fields |
| 100 | HmacValidator |
Timing, request method order and the signed token |
Installation
composer require jkphl/antibot
Requires PHP 7.4 or later. It has no runtime dependencies beyond two PSR interfaces.
Usage
use Jkphl\Antibot\Ports\Antibot; use Jkphl\Antibot\Ports\Validators\HmacValidator; use Jkphl\Antibot\Ports\Validators\HoneypotValidator; use Jkphl\Antibot\Ports\Validators\ParameterPlausibilityValidator; $antibot = new Antibot($sessionId, 'contact-form'); $antibot->setSecret($installationSecret); $antibot->addValidator(new HoneypotValidator(['email' => 'email', 'url' => 'url'])); $hmac = new HmacValidator(); $hmac->setMethodVector(HmacValidator::METHOD_GET, HmacValidator::METHOD_POST); $hmac->setSubmissionTimes(3600, 3, 1); $antibot->addValidator($hmac); $antibot->addValidator(new ParameterPlausibilityValidator([ 'message' => ['minWords' => 5, 'maxLength' => 5000], ])); // When rendering the form echo $antibot->armor($request); // When a submission arrives $result = $antibot->validate($request); if (!$result->isValid()) { // Refused — $result->getErrors() says why }
isValid() is deliberately false for a submission Antibot never saw: a request without
any armor is skipped, not passed.
Please find the full documentation in the doc directory.
Dependencies
What an application takes on by requiring this library — direct runtime dependencies,
no development tools. The graph is generated from composer.json by composer depgraph,
and the pipeline refuses any commit in which it has gone stale.
graph LR
root(["jkphl/antibot"])
p0["php<br/>^7.4 || ^8.0"]
r1["psr/http-message<br/>^1.1 || ^2.0"]
r2["psr/log<br/>^1.1 || ^2.0 || ^3.0"]
root --> p0
root --> r1
root --> r2
Loading
Quality
composer test # PHPUnit composer check-style # PHP_CodeSniffer (PSR-12) composer mess # PHPMD
The same three run in the pipeline, against PHP 7.4. The code sniffer and the mess detector are configured to be met, not argued with: their findings are fixed, never excluded.
Contributing
Found a bug or have a feature request? Please see contributing and conduct for details.
Security
If you discover any security related issues, please email joschi@tollwerk.de instead of using the issue tracker.
Credits
License
Copyright © 2026 Joschi Kuphal / joschi@tollwerk.de. Licensed under the terms of the MIT license.