bel-consulting / kiwicaptcha-symfony
Self-contained Symfony bundle for KiwiCaptcha proof-of-work CAPTCHA. No external services, no tracking — challenges are issued and verified locally via the kiwicaptcha/kiwicaptcha-php core.
Package info
github.com/Bel-Consulting-OU/kiwicaptcha-symfony
Type:symfony-bundle
pkg:composer/bel-consulting/kiwicaptcha-symfony
Requires
- php: >=8.1
- ext-json: *
- ext-openssl: *
- ext-sodium: *
- kiwicaptcha/kiwicaptcha-php: ^1.0
- kiwicaptcha/kiwicaptcha-risk-php: ^1.0
- predis/predis: ^3.5
- psr/cache: ^3.0
- psr/log: ^3.0
- symfony/config: ^6.0|^7.0|^8.0
- symfony/console: ^6.0|^7.0|^8.0
- symfony/dependency-injection: ^6.0|^7.0|^8.0
- symfony/form: ^6.0|^7.0|^8.0
- symfony/framework-bundle: ^6.0|^7.0|^8.0
- symfony/http-foundation: ^6.0|^7.0|^8.0
- symfony/http-kernel: ^6.0|^7.0|^8.0
- symfony/routing: ^6.0|^7.0|^8.0
- symfony/twig-bridge: ^6.0|^7.0|^8.0
- symfony/validator: ^6.0|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^10.5
- symfony/browser-kit: ^6.0|^7.0|^8.0
- symfony/cache: ^6.0|^7.0|^8.0
- symfony/dom-crawler: ^6.0|^7.0|^8.0
- symfony/error-handler: ^6.0|^7.0|^8.0
- symfony/mailer: ^6.4|^7.1|^8.1
- symfony/property-info: ^6.0|^7.0|^8.0
- symfony/security-core: ^6.0|^7.0|^8.0
- symfony/security-http: ^6.0|^7.0|^8.0
- symfony/twig-bundle: ^6.0|^7.0|^8.0
- symfony/yaml: ^6.0|^7.0|^8.0
- twig/twig: ^3.0
- web-auth/webauthn-lib: ^5.3.5
Suggests
- symfony/security-core: Required for the step-up plane and the first-attempt login guard (TokenInterface, voters).
- symfony/security-http: Required for AuthenticationTokenCreatedEvent (the first-attempt login guard).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-30 22:05:39 UTC
README
A self-contained, privacy-preserving anti-abuse integration for
Symfony 6, 7 and 8. KiwiCaptcha is an authenticated one-shot
proof-of-work system combined with an adaptive anti-abuse layer. The
layer spans risk assessment, resource controls, authenticated
challenge-bound decoys as probabilistic automation evidence,
replay-resistant state, transaction/IP binding, chained challenges,
security epochs, key rotation and revocation, SiteVerify and migration
compatibility, and first-party privacy. The bundle makes no third-party
requests and no third-party tracking: challenges are issued and
verified locally by the
kiwicaptcha/kiwicaptcha-php core (HMAC-SHA256 signing, IP binding,
single-use storage, SHA-256 + Argon2id proof-of-work). The widget
inlines its own CSS, WASM solver, and driver, so no request ever leaves
your application, and the secret key never leaves your server. The
precise scope of these claims, with their assumptions and the tests
that evidence them, is in docs/claims-registry.md
and docs/privacy.md.
This bundle is the only Symfony integration of KiwiCaptcha. The
economic core: every signup, login, reset or scraping attempt carries a
real, tunable computational cost that makes mass abuse uneconomical.
No human solves the challenge by hand: the visitor's CPU does the
work, and an automated client's CPU can do the same work. Browser
behavioral telemetry is client-controlled and forgeable, a
supplementary signal, never the security boundary.
SECURITY.md states authoritatively what
KiwiCaptcha does and does not protect against.
Decoys
The adaptive layer can arm an authenticated, challenge-bound decoy field (protocol v3, or v4 when the execution dimension rides along) that generic automation may fill. The decoy is signed into the challenge record, and the verification checks it together with the record. It is probabilistic automation evidence, not a sole security boundary: the security property of the product is the proof-of-work cost, and knowledge of the architecture does not invalidate KiwiCaptcha's core security guarantees. The exact decoy vocabulary, DOM variants, scoring weights, escalation thresholds and classifiers are intentionally not published; adaptive parameters are a moving target that silence buys time for.
Privacy model
The default posture is strict privacy: telemetry off, same-origin enforcement on, the timing heuristic off, and the coarse client-context opt-in refused. Where the system keeps state about a source, it keeps only keyed ephemeral pseudonyms (HMAC-derived, epoch-rotating, never a raw IP or a stable identifier). No canvas, audio, or GPU signal exists anywhere in the product. The experimental execution dimension may transmit challenge-scoped, low-entropy browser-layout evidence: the observed height of a text line rendered with the engine's default font, inside the armed execution trace only. Privacy Strict mode disables the dimension entirely. The privacy contract and the strict-mode consequences are in docs/privacy.md.
Accessibility
The widget is keyboard-operable and screen-reader safe, with WCAG 2.2
AA evidence, scope and limitations in
Resources/ACCESSIBILITY.md.
Quick start
Package installation status. This bundle has a dedicated public source repository tagged
v1.0.0(Bel-Consulting-OU/kiwicaptcha-symfony). It is not yet registered on Packagist (bel-consulting/kiwicaptcha-symfony), socomposer require bel-consulting/kiwicaptcha-symfonywill not resolve from Packagist until that one-time submit lands. Until then install from the package repository (VCS), then follow docs/getting-started.md. The Flex recipe (recipes-contrib PR #2038) is also still open.
Install, register, and configure in five minutes:
docs/getting-started.md (profile-first:
protection_profile + secret + public URL + Redis, then
kiwicaptcha:doctor); the full configuration reference is
docs/configuration.md.
Documentation
| Document | Covers |
|---|---|
| getting-started.md | Install, environment placeholders, protection profiles, form/template usage, the jti and (jti, action) idempotency contract, the doctor |
| flex-recipe.md | The Flex recipe template: contents, publishing, and the manual equivalent |
| configuration.md | Protection profiles and every configuration key with defaults and validation |
| security-hardening.md | The integration layer: what application teams must do (profiles, rollout, widget lifecycle, deployment basics, doctor) |
| operations.md | SECURITY-MAINTAINER material: rate-limit internals, admission math, failover replay safety, protocol rollouts, transport guidance |
| risk-engine.md | The adaptive risk engine: pre-issue assessment, post-solve checks, escalation, application hooks |
| chained-challenges.md | Two-stage chained challenges: obligations, tickets, stage 1 / stage 2 |
| siteverify.md | The provider-shaped /siteverify endpoint, idempotency and crash recovery |
| migration.md | Migrating from reCAPTCHA / hCaptcha / Turnstile and from the legacy bundled Symfony layer |
| privacy.md | The privacy contract: privacy modes, pseudonymous identities, telemetry, client context, log redaction |
| troubleshooting.md | Public violation codes and common failure modes |
| glossary.md | One-line definitions of the protocol and policy terms |
| claims-registry.md | Externally visible security, privacy and compliance claims with their scope, assumptions and owning test |
Lifecycle and invariants
Issuance signs a challenge record (scope, TTL, policy epoch, issuer, key id, nonce-bound binding tag), stores it, and hands the widget a solver-friendly challenge. Verification runs cheap structural checks first, then an atomic pending-to-consumed transition, and derives the proof exactly once. The consumed record is retained with its deterministic result, so a replay reproduces the outcome instead of re-deriving. The security epoch, the keyring (current kid plus historical secrets and revoked kids) and the deployment issuer/region are enforced at verification, so rotation and emergency revocation are configuration actions, not redeploys. These invariants are pinned by the tests in docs/claims-registry.md.
Integrations
kiwicaptcha/kiwicaptcha-php: the framework-neutral PHP core this bundle wraps.kiwicaptcha/kiwicaptcha-risk-php: the adaptive risk engine (cross-language risk-v1 contract, byte-identical with the Rust implementation), used by the bundle's optional risk layer. See risk-engine.md.- Rust core + WASM solver assets: the widget markup/CSS/JS has a single
source of truth in the Rust repository; keep the bundled copies in sync
with
bin/sync-assets.sh. See operations.md. - Protocol: the canonical risk-v1 state protocol lives in protocol/risk-v1.
- Accessibility: WCAG 2.2 AA evidence, scope and limitations in
Resources/ACCESSIBILITY.md.
Migrating an incumbent integration
kiwicaptcha:migrate scans a codebase for incumbent captcha
integrations (reCAPTCHA, hCaptcha, Turnstile, Altcha, Friendly
Captcha) and emits the shim config: the client script block, the
sitekey-to-scope mapping table and the server-side siteverify swap
instructions.
bin/console kiwicaptcha:migrate /path/to/app --format=text # or json
bin/console kiwicaptcha:migrate /path/to/app --out=plan.md --dry-run
The scan is read-only and bounded: known text extensions, files up to
512 KiB, and the dependency and build trees are skipped. Lines carrying
kiwi's own markers (the shim attributes, kiwi__token, kiwi route
paths) never count as findings, so an already migrated tree scans
clean.
Related
SECURITY.md: the authoritative security document for the whole product.- The PHP core's own documentation and the product root
READMEcover the framework-neutral and Rust sides.
What it is not
- Proof of computation, rather than proof of human. KiwiCaptcha verifies that a client spent CPU time, not that a human did. Any automated client that pays the same cost passes.
- Telemetry is forgeable by design. A custom client can omit or fake
anything the widget reports. Treat telemetry as a supplementary signal,
never the security boundary. Under strict privacy mode the widget
collects nothing (
telemetry: off). - IP binding is best-effort. Because IPs legitimately change behind
NAT/proxies, strict binding would reject real users. Operators can
disable the check entirely (
binding_mode: noneissues challenges with an empty binding tag; the core BindingMode is fully wired through the DI layer); it is a relay mitigation rather than a guarantee. - Server-side timing is a heuristic, off by default. The minimum-duration
floor is measured by your server, so a client cannot buy its way out of
it, but the server clock must be correct, and strict privacy mode
disables the floor entirely (
min_duration_ms: 0). - The WASM solver and its JS fallback ship as open source. An attacker can always write their own solver or reuse the shipped source. The value is the cost per attempt, not the impossibility of solving.
License and copyright
MIT — Copyright (c) 2026 Bel Consulting OÜ.