olivcode / form-guard
Progressive abuse protection for PHP forms, with optional JavaScript signals.
Requires
- php: ^8.3
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.90
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Form Guard
Progressive abuse protection for PHP forms, with optional JavaScript signals.
PHP 1.0 library, with a separate Laravel 13 adapter. See the compatibility contract, installation guide and security policy.
Form Guard combines session-bound form tokens, randomized field names, a honeypot, server-side timing, request limits, progressive bans and optional browser signals. It does not send email and has no mandatory framework, CAPTCHA provider or external service. Turnstile is an explicit optional plugin.
Requirements
- PHP 8.3 or later within the PHP 8 series.
- Writable private local storage with reliable file locking.
- A session managed by your application.
- JavaScript is optional. Browser signals are an additional heuristic, not proof that a visitor is human. Node.js is only used for development tests, not PHP runtime.
Install in your application
Choose Composer installation or standalone PHP installation. Both use the same engine and protections. The manual method needs no Composer executable or vendor/ directory: copy the PHP library and load its bundled autoloader. The guide covers private storage, optional JavaScript, maintenance and updates.
Development installation
Clone this repository, then run:
composer install
composer check
composer test
composer test:concurrency
composer test:browser
composer test:failure
composer test:load
composer test:distribution
npm ci --ignore-scripts
npx playwright install chromium firefox webkit
npm run test:e2e
Without a local Composer executable, the test runners can also be invoked directly:
php tools/check.php php tests/run.php php tests/storage.php php tests/maintenance.php php tests/http.php php tests/concurrency.php php tests/storage-concurrency.php node tests/browser.mjs
olivcode/form-guard is the proposed Composer package name, not an existing published package. The development repository is olivCode/form-guard. composer test:distribution builds and audits the ZIP, then installs it into a fresh local consumer with Packagist disabled. Distribution from a separate public repository will be prepared later.
Integration overview
use FormGuard\Guard; use FormGuard\Rejection; require __DIR__ . '/vendor/autoload.php'; // Start your application's secure session before output. $_SESSION['contact_forms'] ??= []; $guard = new Guard([ 'scope' => 'contact', 'locale' => 'fr', // Optional: an absolute PRIVATE storage directory, outside the web root. // 'runtime_dir' => '/var/lib/my-app/form-guard', ]); // On GET: issue a form, render its tokens, fields and signals. $form = $guard->issueForm($_SESSION['contact_forms'], ['name', 'email', 'message']); // On POST, BEFORE validating the request body or business fields: $guard->beginRequest( $_SERVER['REMOTE_ADDR'] ?? 'unknown', $_SERVER['HTTP_USER_AGENT'] ?? '', session_id(), ); $verified = $guard->validateForm($_POST, $_SESSION['contact_forms']); // Validate email, lengths and other application-specific rules here. // Consume immediately before performing the action; keep the session locked. $guard->consumeForm($_SESSION['contact_forms'], $verified['id']); // Your application can now send its email or process the submission.
The GET and POST lines above illustrate separate request branches. For a complete runnable implementation with safe HTML output and error handling, see examples/index.php. For AJAX refreshes, use publicForm() and then window.FormGuard.refresh(formElement) after updating tokens and field names. See integration details.
Runnable example
php -S 127.0.0.1:8000 -t examples
Open http://127.0.0.1:8000. The example can run without Composer and never sends email. Keep it bound to loopback; it is an integration example, not a production contact service.
Default policy
- Up to 20 outstanding forms per session, expiring after two hours.
- Three-second minimum submission time, computed from server-held state.
- Five POST attempts per IP + user agent + session within ten minutes.
- Thirty POST attempts per IP within ten minutes, across sessions and agents.
- Progressive client bans of 1, 2, 4, 8, 16, 32, then at most 60 minutes.
- Shared-IP limits throttle for the remaining window by default; progressive IP bans are opt-in.
- Every POST attempt counts, including invalid tokens and field errors.
- Form tokens are consumed before an action to prevent replay.
These defaults are intended for low-volume contact forms. Shared connections and repeated honest input errors can hit the same limits; adjust the policy to your actual traffic. See configuration.
Boundaries
This is application-level protection. It does not filter email inboxes, guarantee spam-free submissions, provide DDoS protection or replace validation, secure sessions and hosting controls. JavaScript signals can be forged and the native no-JS mode can be simulated. Patient or distributed bots may pass the checks.
The file backend uses validated private files, atomic state replacement and explicit recovery. Workers must share reliable local locking and a private state directory. It is not a general distributed rate limiter. IP, user agent and session identifiers are represented by keyed HMACs; those values are pseudonymous, not anonymous. Expired entries are pruned on later requests, not by a scheduled deletion job.
Project
- HTTP security and optional Turnstile
- 1.0 API and lifecycle compatibility
- Plugins and dependencies
- Scenario/component recommendations
- Profiles and grouped configuration
- Additional rules
- Internationalization and structured rejections
- Storage and scheduled maintenance
- Quality, browser tests and load/failure tests
- Manual accessibility and external developer acceptance
- Development instructions
- Security policy
- Contributing
MIT License. Copyright (c) 2026 Olivier Gaillot.