A lightweight plugin to stop bot registrations on WordPress and WooCommerce sites without relying on reCaptcha.
I built this after seeing the same problem play out repeatedly on client sites: botnets creating thousands of dormant user accounts, waiting for a plugin vulnerability to exploit. The usual solution is reCaptcha, but that’s a privacy nightmare and a pain to manage. So I took a different approach: three independent layers of defence that work together to block automated registrations while keeping the sign-up process smooth for real customers.
What it does
Registration Guard uses three layers to stop bot registrations:
JavaScript nonce challenge: The registration form requires a time-delayed security nonce fetched via AJAX after page load. Bots that POST directly to the registration handler without loading the page in a browser are blocked instantly. The nonce endpoint itself validates referrer headers, enforces a minimum elapsed time (default: 1 second), and rate-limits requests per IP address.
Double opt-in: New registrations must verify their email address via a tokenised verification link. Unverified accounts are automatically deleted after a configurable window (default: 24 hours). If an unverified user tries to log in, they see a clear “please verify your email” notice with an option to resend the verification email. Rate limiting prevents abuse of the resend mechanism (3 attempts per 15 minutes).
Geo-restriction: If you only serve specific regions, you can block or allow registrations based on the user’s country. This requires a geo-IP provider (WooCommerce is the most common, but any plugin can supply geolocation data via a filter hook). You can configure either an allowlist or blocklist of countries using ISO country codes.
Each layer works independently. You can disable any layer you don’t need.
How it works
The nonce challenge enqueues a small JavaScript file (assets/js/nonce-challenge.js) on pages with registration forms. After a short delay (configurable, defaults to 1 second), the script makes an AJAX request to fetch a time-limited nonce and injects it into a hidden form field. On form submission, the server validates the nonce. Missing or invalid nonces trigger a rejection with a user-friendly error message. The nonce endpoint validates referer headers and rate-limits requests using transients keyed by IP address.
Email verification uses hashed tokens stored in user meta. When a user registers, the plugin generates a 32-character URL-safe token, hashes it with wp_hash_password(), and stores the hash in _rg_verification_token meta. A verification email is sent with a link like site.com/?rg_verify={user_id}:{token}. When clicked, the plugin validates the token using wp_check_password() against the stored hash, marks the account as verified, and redirects the user to set their password. Tokens are single-use and expire after the configured window.
A WP-Cron job runs hourly to delete unverified accounts that have expired. It processes in batches (50 at a time) and only deletes users with safe roles (customer, subscriber). Administrators, editors, and shop managers are never touched. All deletions are logged to a custom database table for audit purposes.
Geo-restriction uses a filter-based provider model. Any plugin can supply geolocation data by hooking into registration_guard_geolocate_ip. WooCommerce is detected automatically and its WC_Geolocation::geolocate_ip() function is used if available. On registration submission, the plugin checks the user’s country code against the configured allowlist or blocklist. If blocked, registration fails with a generic error message.
Customising behaviour
The plugin provides several filter hooks for customising behaviour.
Skip verification for specific users
Use the registration_guard_skip_verification filter to bypass email verification for specific users or contexts:
add_filter( 'registration_guard_skip_verification', function( $skip, $user_id ) {
// Skip verification for users with a specific role
$user = get_userdata( $user_id );
if ( $user && in_array( 'wholesale_customer', $user->roles, true ) ) {
return true;
}
return $skip;
}, 10, 2 );Geo-IP lookups
If you’re a hosting provider or plugin developer, you can supply geolocation data to Registration Guard via the registration_guard_geolocate_ip filter:
add_filter( 'registration_guard_geolocate_ip', function( $country_code, $ip_address ) {
// Example: Use MaxMind GeoIP2 database
$reader = new GeoIp2\Database\Reader( '/path/to/GeoLite2-Country.mmdb' );
try {
$record = $reader->country( $ip_address );
return $record->country->isoCode;
} catch ( Exception $e ) {
return $country_code; // Fall back to default
}
}, 10, 2 );See docs/geo-ip-providers.md for detailed guidance on implementing a geo-IP provider.
Customise post-verification redirect
By default, verified users are redirected to the WordPress password reset form. You can customise this with the registration_guard_verification_redirect_url filter:
add_filter( 'registration_guard_verification_redirect_url', function( $url, $user_id ) {
// Redirect to a custom welcome page
return home_url( '/welcome/' );
}, 10, 2 );Adjust nonce challenge timing
The front-end nonce challenge script can be configured via the registration_guard_nonce_script_data filter:
add_filter( 'registration_guard_nonce_script_data', function( $script_data ) {
// Increase the delay before fetching the nonce
$script_data['delay'] = 3; // seconds
return $script_data;
} );Customise IP detection for proxy setups
If your site sits behind a trusted reverse proxy (like Cloudflare or a CDN), you can override IP detection with the registration_guard_client_ip filter:
add_filter( 'registration_guard_client_ip', function( $ip ) {
// Trust Cloudflare's CF-Connecting-IP header
if ( isset( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
}
return $ip;
} );Security and privacy
Access control: The nonce endpoint is public (it has to be, for unauthenticated registration forms), but it’s rate-limited per IP address using transients. The resend verification email endpoint requires authentication and validates WordPress nonces to prevent CSRF attacks.
Token security: Verification tokens are generated using random_bytes() for cryptographic strength, then hashed with wp_hash_password() before storage. Plaintext tokens are never stored in the database. Verification links consume the token on first use and are only valid within the configured time window.
Email suppression: When double opt-in is enabled, the plugin suppresses WordPress core’s “set your password” email and WooCommerce’s “new account” email. Verified users are redirected to the password reset form to set their password in a single-email flow. This reduces email noise and improves the user experience.
Data cleanup: Unverified accounts are automatically deleted by the hourly cron job. Only users with safe roles (customer, subscriber) are deleted. All deletions are logged to a custom database table for audit purposes. The uninstall.php handler removes all plugin data when the plugin is deleted: options, user meta, transients, the log table, and cron hooks.
IP detection: By default, the plugin uses REMOTE_ADDR for IP address detection. Proxy headers (X-Forwarded-For, HTTP_CF_CONNECTING_IP, etc.) are used as a fallback only and are not trusted by default. Use the registration_guard_client_ip filter if your site sits behind a trusted reverse proxy.
Technical notes
Registration Guard is built with WordPress standards and modern PHP practices:
- PHP 8.0+: Type hints, return types, and null coalescing operators throughout
- WordPress 6.0+: Uses modern WordPress APIs (Settings API, admin notices API, etc.)
- No inline JavaScript: All JS lives in external files under
assets/js/ - No database tables (except logs): Uses existing WordPress structures for user meta, options, and transients
- WooCommerce optional: All features degrade gracefully when WooCommerce is not active
- Integration architecture: Third-party plugins can integrate via the
integrations/pattern and reuse core functionality - Event logging: All registration attempts, blocks, and verifications are logged to a custom table with automatic pruning (default: 30 days retention)
The plugin follows WordPress Coding Standards and is validated with phpcs using the WordPress ruleset.
What’s next
- Admin dashboard widget showing recent registration blocks
- Email notifications for admins when suspicious registration patterns are detected
- Integration with popular membership plugins (MemberPress, Restrict Content Pro, etc.)
- WP-CLI commands for bulk verification or account cleanup
- Export registration logs to CSV for security audits
