jkphl/antibot

Death to spambots! CAPTCHA-less form validation

Maintainers

Package info

github.com/jkphl/antibot

pkg:composer/jkphl/antibot

Transparency log

Statistics

Installs: 151

Dependents: 1

Suggesters: 0

Stars: 2

Open Issues: 1

v2.1.0 2026-09-01 07:17 UTC

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 &#124;&#124; ^8.0"]
    r1["psr/http-message<br/>^1.1 &#124;&#124; ^2.0"]
    r2["psr/log<br/>^1.1 &#124;&#124; ^2.0 &#124;&#124; ^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.