Skip to content

0.3.0: stronger PoW, replay protection, opt-in spam layers, email validation, tests#2

Merged
oxyc merged 8 commits into
mainfrom
feat/replay-protection-and-cost
Jun 2, 2026
Merged

0.3.0: stronger PoW, replay protection, opt-in spam layers, email validation, tests#2
oxyc merged 8 commits into
mainfrom
feat/replay-protection-and-cost

Conversation

@oxyc
Copy link
Copy Markdown
Member

@oxyc oxyc commented Jun 2, 2026

Summary

Builds on the opt-in settings (0.2.0) with a layered, first-party anti-bot/anti-spam upgrade — every layer is opt-in and tuned to never block real users (the fuzzy layers mark spam, recoverable, rather than rejecting).

Proof-of-work

  • Protection strength preset dropdown (Low/Standard/High/Very high), site-wide and per-form; default Standard is ~20× the old cost. Exact values via the cost filter.
  • Replay protection — each solved challenge is single-use (deduped on its signature for its lifetime), so a proof can't be replayed across many submissions.

Spam layers (global toggles, apply to all forms; mark-as-spam, never reject)

  • Rate limiting — default 3/hour/IP, configurable count + window. IP resolved independently of GF (which sites blank for GDPR), kept only as a salted HMAC in a transient — raw IP never stored/logged. CDN client-IP header configurable.
  • Content heuristics — definite-spam keywords (word-boundary matched) + accumulating weaker signals (link farms incl. scheme-less URLs, a URL in the name field, injected markup, wrong-script text). Scans all visitor text incl. composite name/address fields; ignores zero-width evasion. Single weak signals never flag alone.

Email validation (blocks, to help the visitor fix a bad address)

  • Verifies via Bouncer (BOUNCER_API_KEY). Per-verdict checkboxes: undeliverable (default on), risky (off), disposable (on). Fails openunknown/missing key/API error never block. Sends email to a third party (privacy note in README).

Tests

  • unit + mocked (Brain\Monkey) suites — 50 tests, run in CI on PHP 8.2–8.4 (no DB/GF): Bouncer HTTP mapping/caching/fail-open, rate-limit counter, replay dedup, content scoring, IP resolver, cost clamp.
  • integration (wp-phpunit) — real WP+GF for settings/spam-layers/cost; skips without GF (CI), runs via wp-env/DDEV. Adds a wp-env CI smoke job. phpunit pinned to ^9 (wp-phpunit).

⚠️ Notes for review

  • Bumps to 0.3.0.
  • The unit + mocked suites are verified locally; the integration suite / wp-env job were not executed in dev (need GF + a WP test DB) — please confirm on the first CI run.
  • After merge: cut a v0.3.0 release, then bump snellman to ^0.3 (+ raise the e2e poll timeout for the higher PoW cost).

🤖 Generated with Claude Code

oxyc and others added 8 commits June 2, 2026 12:19
Builds on the opt-in settings with several anti-bot improvements (0.3.0):

- Proof-of-work cost is now a "Protection strength" preset dropdown
  (Low/Standard/High/Very high), settable site-wide and per form, defaulting to
  a 20x stronger Standard (200k). Exact values still available via the
  `genero/gravityforms_altcha/cost` filter.
- Replay protection: each solved challenge is single-use (deduped on its unique
  signature for the rest of its lifetime), so a proof can't be replayed across
  many submissions.
- Optional, independent spam layers (global toggles, applied via
  gform_entry_is_spam so flagged entries are marked spam — recoverable — never
  rejected):
  - Rate limiting (default 2/min/IP). IP resolved independently of GF (which
    sites often blank for GDPR) and kept only as a salted HMAC in a 60s
    transient; never stores the raw IP. CDN client-IP header is configurable.
  - Content heuristics: definite-spam keywords flag immediately; weaker signals
    (link farms, injected markup, wrong-script text) must combine to flag.

Unit tests cover the fingerprint, cost clamp, IP resolver, and content scoring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the fixed per-minute allowance with a configurable max + window
(genero/gravityforms_altcha/rate_limit_max + _window), defaulting to 3 per hour.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an "Email validation" toggle that rejects undeliverable or disposable
email addresses at the field with a corrective message, so visitors fix a bad
address instead of silently never being reachable. Verifies via the Bouncer
API (BOUNCER_API_KEY env var, or the bouncer_api_key filter).

Fails open: risky/unknown verdicts, a missing key, or any API error never
block. Definitive verdicts are cached for a day keyed by a hash of the email.
Sends the email to a third-party service — document in your privacy policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y/disposable)

Replaces the hardcoded "undeliverable or disposable blocks" with admin checkboxes
under the Email validation toggle, so each Bouncer verdict can be acted on
independently. Defaults: undeliverable + disposable on, risky off (risky can
reject some real catch-all/role addresses). Unchecked/unset verdicts never block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Scan all visitor free text, including composite name/address sub-inputs
  (previously only top-level text/textarea — the Name field, where bots love to
  dump links, was invisible).
- A URL in the name field is treated as near-certain spam.
- Count scheme-less www. links and graduate link scoring (1–2 innocent, 3–4 a
  signal, 5+ damning) without double-counting http://www.
- Match definite keywords on unicode word boundaries (no Scunthorpe false
  positives if a short keyword is configured).
- Normalise zero-width / invisible characters before matching to defeat simple
  evasion.

Single weak signals still never flag alone, so false positives stay near zero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Splits the suite into three layers, mirroring the house pattern (gds-mcp):

- unit: pure logic (existing), runs everywhere.
- mocked: WP-glue tested with WordPress functions stubbed via Brain\Monkey —
  EmailValidator's Bouncer HTTP→verdict mapping/caching/fail-open, the
  rate-limit counter + unknown-IP guard, and replay dedup. Runs in CI (no DB,
  no GF) — `composer test` now covers unit + mocked.
- integration: real WordPress via wp-phpunit (`composer test:integration`),
  exercising the GF settings, the gform_entry_is_spam spam layers, and cost
  resolution against an actual Gravity Forms install. GF is commercial, so these
  skip when it's absent (CI) and run via wp-env / DDEV.

Pins phpunit to ^9 (wp-phpunit requirement) and adds a wp-env CI job that
smoke-loads the plugin in real WordPress. .wp-env.json mounts a sibling
gravityforms checkout for local integration runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… the matrix

The lock was generated on PHP 8.4 and pinned doctrine/instantiator 2.1.0 (needs
^8.4), breaking composer install on the 8.2/8.3 CI runners.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s on DDEV/wp-env)

The integration suite needs Gravity Forms, which is commercial and absent in CI,
so the job only smoke-booted wp-env and skipped every test — not worth the Docker
flakiness. Integration runs locally via 'composer test:integration' (wp-env/DDEV).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@oxyc oxyc merged commit 804c9eb into main Jun 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant