Setyenv · Docs
  • English
  • Spanish
  • Chinese
  • Japanese
  • Arabic
  • German
  • French
  • Hindi
  • Indonesian
  • Italian
  • Dutch
  • Portuguese
  • Russian
  • Turkish

Hardening anonymous writes

When an entity lists public in its can_create_roles column, unauthenticated visitors can POST records to it. Three layers of defence apply, in order from cheapest to most invasive:

1. Honeypot field (pfm_hp)

Every [pfm_form] render emits a hidden <input name="pfm_hp"> inside a .pfm-hp-wrap block (CSS-hidden via display:none + off-screen). Real visitors never see or fill it. Simple bots eagerly fill every input — the server rejects with HTTP 422 (pfm_honeypot_tripped) when the value is non-empty.

Cost: one server-side trim() per submission. No JS, no third-party dependency.

2. Per-IP rate limit

AnonymousGuard::check_rate_limit() uses a WordPress transient keyed pfm_arl_<sha1(entity|ip)> to count anonymous submissions per entity per IP per window. Defaults: 30 submissions / 3600 s.

Customise per-entity via the pfm_anonymous_rate_limit filter:

add_filter('pfm_anonymous_rate_limit', function ($cfg, $slug) {
    if ($slug === 'lead') {
        return ['window_seconds' => 60, 'max_per_window' => 5];
    }
    return $cfg;
}, 10, 2);

Customise the global defaults via pfm_anonymous_default_rate_limit.

Excess submissions get HTTP 429 (pfm_rate_limited) with the configured limit + window in the error data so the front-end can show a friendly retry message.

IP detection walks X-Forwarded-For (first value), X-Real-IP, then $_SERVER['REMOTE_ADDR'].

3. CAPTCHA hook

The pfm/captcha/verify filter runs after honeypot + rate-limit. Default chain accepts. Wire in Cloudflare Turnstile, hCaptcha or any other provider:

add_filter('pfm/captcha/verify', function ($ok, $entity, $payload, $request) {
    $token = $payload['cf_token'] ?? '';
    return $ok && my_turnstile_verify($token, $request);
}, 10, 4);

A failure here returns HTTP 422 (pfm_captcha_failed).

Logged-in bypass

All three layers skip when get_current_user_id() > 0. Logged-in users have already passed the WordPress nonce check at the REST layer, so the anti-spam layers are unnecessary friction.