LW Firewall registration guard
SkillFiles & storageIntegrate custom WordPress registration forms and signup REST endpoints with LW Firewall's registration honeypot, signed timing token, single-use storage, rejection tracking, and rate limiting. Use when code creates users outside the core `wp-login.php?action=register` flow or references `RegisterGuard`, `RegisterToken`, `RegisterTracker`, `lw_fw_reg_token`, `lw_fw_url`, `registration_errors`, `wp_insert_user`, public registration REST routes, proof-of-render, honeypots, replay protection, or registration auto-bans.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the LW Firewall registration guard skill
What this skill tells your AI
The instructions your AI receives, as published by lonsdale201/wp-agent-skills in lw-plugins/lw-firewall-registration-guard/SKILL.md and read by ahel’s review.
Use this skill when a companion plugin owns the signup UI or transport. LW Firewall automatically wires only the core WordPress registration form; a custom PHP, AJAX, headless, WooCommerce, CRM, LMS, or REST flow must opt in.
Read registration-and-rest-integration.md before implementing a JSON endpoint or copying the manual validation adapter.
Automatic coverage boundary
The plugin registers RegisterGuard only when all of these are true:
- the MU worker is installed and matches
LW_FIREWALL_VERSION; - the master
enabledoption is true; register_protect_enabledis true;- WordPress
users_can_registeris true.
It then hooks register_form for rendering and registration_errors for
validation. A custom route that calls wp_insert_user() does not pass through
this guard automatically. Decide separately whether the custom product should
honour users_can_register.
Verified public surface
| Contract | Current behavior |
|---|---|
RegisterGuard::render_fields() | Echoes lw_fw_reg_token and, when enabled, lw_fw_url |
RegisterGuard::validate( $errors = null, $login = '', $email = '' ) | Reads the current $_POST, records rejection, adds a generic error |
RegisterToken::issue( string $scope = 'reg' ) | Returns a signed per-render token |
RegisterToken::make( int $issued, string $scope = 'reg', string $nonce = '' ) | Deterministic seam for tests |
RegisterToken::verify( $token, $min, $max, ?$storage = null, $scope = 'reg' ) | Checks signature, scope, age and optional atomic single use |
RegisterToken::check( $token, $now, $min, $max, ?$storage = null, $scope = 'reg' ) | Same, against an explicit "now" |
validate() deliberately does not hard-type its first parameter (1.5.6):
another plugin on registration_errors returning a non-WP_Error used to cause
an uncatchable TypeError on a public form.
| RegisterTracker::record_reject() | Counts non-whitelisted rejected registrations and may write a shared ban |
RegisterGuard's field constants and spam predicate are private. Do not call
private methods or edit the copied MU worker.
Classic server-rendered form
Respect the plugin settings because render_fields() and validate() do not
self-check the master or registration toggles:
use LightweightPlugins\Firewall\Options;
use LightweightPlugins\Firewall\Rules\RegisterGuard;
$lw_guard_enabled = class_exists(RegisterGuard::class)
&& class_exists(Options::class)
&& (bool) Options::get('enabled', true)
&& (bool) Options::get('register_protect_enabled', true);
if ($lw_guard_enabled) {
RegisterGuard::render_fields();
}
Before creating the user:
$errors = new WP_Error();
if ($lw_guard_enabled) {
$errors = RegisterGuard::validate($errors);
}
if ($errors->has_errors()) {
return $errors;
}
// Validate the remaining business fields, then create the user.
This convenience path is valid only when the request data is in $_POST.
REST and headless registration
RegisterGuard::validate() does not read WP_REST_Request; a JSON body can
therefore fail even when it contains the fields. Extract request parameters and
call RegisterToken::verify() manually, using the fixed registration scope
reg and RegisterTracker::record_reject() on failure. The reference contains
a transport-independent implementation.
Mint a token as part of the form/bootstrap response or through a narrowly rate-limited bootstrap route. The token endpoint is public by necessity for an anonymous signup and is not an authentication boundary.
protect_rest_api is only a shared per-IP rate limiter for URLs containing
/wp-json/. It does not validate signup fields, authorize user creation, or
target registration routes specifically. WordPress core's wp/v2/users create
route requires create_users; a deliberately public custom registration route
must implement its own permission policy and abuse controls. Since 1.5.6 the
worker also classifies the bare /wp-json index and the ?rest_route=/...
form, so those no longer slip past the REST bucket.
Security meaning of the token
Keep normal CSRF, capability, authentication, validation, email-verification, and account-policy checks. The LW token is an anti-automation signal, not a WordPress nonce and not proof that a human submitted the form.
Since 1.5.6 the signed payload is v2.<issued>.<scope>.<nonce> with 16 random
bytes of per-render nonce:
- the scope is signed, so a token issued for one form is no longer accepted
by another — and
issue()andverify()must be given the same scope (both default to'reg'; a bareissue()verified against a custom scope fails every time); - tokens issued in the same second are distinct, so two legitimate same-scope renders no longer collide under single use, and a shared page cache no longer hands one token to every visitor;
- the format version is signed, so a token rendered by 1.5.4 fails on 1.5.6 — expect rejections from cached pages right after the upgrade;
- scope is normalized to
[a-z0-9_-]afterstrtolower(), somy form!andmyformare the same scope; - it is still not bound to route, user, IP or field name;
- the honeypot rejects a non-empty value, but an omitted honeypot is still treated as empty.
The token is now a per-render, form-bound proof of render. It is still not proof that a human submitted the form, and not a WordPress nonce.
Auto-ban (fixed in 1.5.5)
RegisterTracker writes ban_<ip> after register_ban_threshold failures.
The 1.5.4 defect — the worker reading shared ban keys only when
auto_ban_enabled or login_limit_enabled was on, so a registration-only
configuration listed a register_spam ban it never enforced — is fixed: the
worker now reads the ban key whenever the firewall is enabled. Still confirm
enforcement with a real follow-up request; the storage key, not the index row,
is the authority.
Review checklist
- Feature-detect LW Firewall and define fail-open or fail-closed behavior.
- Respect
enabledandregister_protect_enabledin custom rendering and validation. - Use
RegisterGuardonly for form-encoded$_POST; adapt JSON explicitly. - Validate the guard before every user/customer/contact/enrollment write.
- Keep field names server-owned and return one generic signup failure.
- Add route-local rate limiting; the global REST toggle is coarse and optional.
- Test valid, missing, filled honeypot, too-fast, expired, replayed, and two same-second token submissions.
- Test
/wp-json/, bare/wp-json, and?rest_route=transports separately. - Verify that a recorded registration ban is actually enforced.
Cross-references
- Use
lw-firewall-custom-form-adapterfor non-registration forms. - Use
lw-firewall-rate-limit-workerfor worker detection and local counters. - Use
lw-firewall-password-reset-protectionfor lost-password flows. - Use
wp-rest-apifor route permissions, schemas, authentication, and errors.
References
- Official project: https://github.com/lwplugins/lw-firewall
- Verified plugin-root-relative sources:
lw-firewall.phpincludes/Plugin.phpincludes/Options.phpincludes/Rules/RegisterGuard.phpincludes/Rules/RegisterToken.phpincludes/Rules/RegisterTracker.phpincludes/Rules/AutoBanner.phpworker/lw-firewall-worker.phpCHANGELOG.md
Signals
- GitHub stars
- 22
- Forks
- 2
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
lw-firewall-registration-guard- Source
- github.com/lonsdale201/wp-agent-skills