Skip to main content

benecaster_follower_signup_limits

Filter Free

Filters the abuse limits applied to the public [benecaster_follower_signup] form. Defaults: 3 submissions per email address per show per hour, 10 per IP per hour, over a one-hour window.

The IP limit is deliberately not scoped per show. A submitter working through every show on the site is the same abuse as one hammering a single show, and a per-show IP bucket would multiply their allowance by the number of shows on the install.

⚠⚠ A limit of 0 or less disables that limit entirely. On this form that is not “unlimited signups” — the form always reports success, sends an email on every submission, and collects no password, so an unlimited version of it is an open mail relay pointed at addresses of the submitter’s choosing, on your domain and your sending reputation. If legitimate traffic is being caught, raise the number rather than removing the limit.

These are filterable and the unlicensed follower cap deliberately is not — that is not an inconsistency. The cap is a licensing gate, where a filter let a site owner grant themselves a bigger free product from a one-line mu-plugin; it shipped with one and had to have it removed. These are abuse limits on the site owner’s own form. Do not cite either as precedent about the other.

Tune the follower signup abuse limits — and know what turning them off costs

Free Intermediate

[benecaster_follower_signup] is a public form that always reports success and sends an email on every submission. It collects no password either, so nothing costs a submitter any effort. Without a limit that is an open mail relay pointed at addresses of the submitter’s choosing, on your domain and your sending reputation — plus a way to exhaust an unlicensed show’s 100-follower ceiling with fake addresses.

The rate limits below are one layer of three. A hidden honeypot field and a minimum render-to-submit time run alongside them, on for every install automatically with no filter or setting to tune. Both share the rate limits’ own silent-drop behaviour: a caught submission shows the same success screen and does nothing. Confirmation-before-counting is a fourth, separate layer — see Recipe: require-follower-signup-confirmation (benecaster_follower_double_optin) — this recipe is about tuning the rate limits specifically, the one layer that is actually configurable.

Defaults: 3 submissions per email address per show per hour, 10 per IP per hour. The IP limit is deliberately not scoped per show, or a submitter’s allowance would be multiplied by the number of shows on the install.

A limit of 0 or less disables that limit entirely. On this form that is not “unlimited signups”, it is an open relay. If you are reaching for 0 because legitimate traffic is being caught, raise the number instead.

A throttled submission looks exactly like a successful one — same screen, nothing provisioned, nothing sent. This is deliberate and must not be “fixed” into a visible error. The per-address limit makes a visible message an activity oracle: submit somebody else’s address, get told you are rate limited, and you have learned that address was recently submitted to this site. That is a worse leak than the address enumeration the identical-response rule exists to prevent, and it would arrive dressed as a usability improvement. Use benecaster_follower_signup_throttled to log refusals instead of showing them.

Behind a reverse proxy. Every per-IP limit counts REMOTE_ADDR, which behind Cloudflare or similar is the proxy’s address — so every visitor shares one bucket and a per-IP limit silently becomes a site-wide one. Opt in explicitly with benecaster_rate_limit_client_ip. Never read a header your own proxy does not overwriteX-Forwarded-For on a site with no proxy in front of it is attacker-controlled. Trusting one turns every per-IP limit in the plugin into no limit at all, and does so silently: the limiter still appears to work, the counters still tick, and every request just looks like a new visitor. This one filter affects the REST limiter as well as this form.

Writing your own limit. Call RateLimitStore::hit( $key, $limit, $window ) — it returns true when the request is over quota. Do not hand-roll another get_transient / set_transient pair; that class exists because the plugin was heading for three private copies of the same window arithmetic. Hash anything personal with RateLimitStore::hash() before it becomes part of a key: transient names land in wp_options on a site with no object cache, and a raw email address there is personal data in a place nobody checks during an erasure request. Full method reference: RateLimitStore.

<?php
/**
 * Loosen the limits for a site running a signup drive.
 *
 * ⚠⚠ A limit of 0 or less DISABLES that limit. On a form that always
 * reports success and mails on every submission, that is an open relay.
 *
 * @param array{address_limit:int, ip_limit:int, window:int} $limits
 */
add_filter( 'benecaster_follower_signup_limits', function ( array $limits, int $show_id ): array {
    if ( 42 === $show_id ) {
        $limits['ip_limit'] = 30;
    }

    return $limits;
}, 10, 2 );

/**
 * Log refusals rather than showing them.
 *
 * ⚠ The submitter is told nothing, on purpose. A visible "too many
 * requests" on a per-address limit tells a stranger that an address was
 * recently submitted here.
 *
 * @param string $reason 'address' or 'ip'.
 */
add_action( 'benecaster_follower_signup_throttled', function ( int $show_id, string $reason ): void {
    error_log( "benecaster: follower signup throttled on show {$show_id} ({$reason})" );
}, 10, 2 );

/**
 * Count the real visitor IP when the site sits behind a reverse proxy.
 *
 * ⚠⚠ Only safe because THIS site's proxy sets and OVERWRITES this header.
 * Reading a client-supplied header instead disables every per-IP limit in
 * the plugin, silently — the counters still tick, every request just looks
 * like a new visitor.
 */
add_filter( 'benecaster_rate_limit_client_ip', function ( string $ip ): string {
    return isset( $_SERVER['HTTP_CF_CONNECTING_IP'] )
        ? (string) $_SERVER['HTTP_CF_CONNECTING_IP']
        : $ip;
} );

/**
 * Writing your own limit — reuse the shared store, do not hand-roll one.
 */
add_action( 'init', function (): void {
    $store = new \Benecaster\Support\RateLimitStore();

    // ⚠ Hash anything personal BEFORE it becomes part of a key: transient
    // names land in wp_options on a site with no object cache.
    $key = 'my_addon_' . $store->hash( $store->client_ip() );

    if ( $store->hit( $key, 5, HOUR_IN_SECONDS ) ) {
        return; // Over quota. Refuse quietly.
    }

    my_addon_do_the_expensive_thing();
} );

View on GitHub →

Parameters

Name Type Default Description
$limits array `array{address_limit:int, ip_limit:int, window:int}` — submissions per address per show, submissions per IP, and the window in seconds.
$show_id int Show being followed. Scopes the address limit, so following two shows from one mailbox is not self-throttling.
$email string The submitted address, for per-address policy. Hashed before it becomes a rate-limit key.

Returns: array

Examples

Loosen the IP limit for one show running a signup drive

add_filter( 'benecaster_follower_signup_limits', function ( array $limits, int $show_id ): array {
    if ( 42 === $show_id ) {
        $limits['ip_limit'] = 30;
    }

    return $limits;
}, 10, 2 );

Notes

Both limits are always recorded, even once one has tripped. An early return on the address limit would leave the IP counter untouched, so an attacker cycling through fresh addresses would never accumulate against the limit actually meant to stop them.

Programmatic callers are not throttled. The limits are applied by the shortcode, not by FollowerSignup, so benecaster_follower_signup() and add-ons doing bulk enrolment are unaffected. A public form and a trusted server-side call are different exposures and deliberately do not share a limit. This is also the answer for a podcaster whose bulk import through the form stopped working: use the import tooling, which is not throttled.

The signed-in path is not throttled either. It sends no mail and a second click mints no second token, so neither abuse applies.