Subtle diagonal pinstripe pattern in light grey and cream, giving an impression of printed editorial paper

Secure Plugin Forms With wp_nonce_field

Plugin forms often sit at the boundary between convenience and trust. A settings screen, importer, or account update can be targeted with cross-site request forgery (CSRF), so a plugin should verify that a request carries a token issued by WordPress. wp_nonce_field() creates the hidden form token for that check without requiring a custom database table.

The function is easy to misuse because its name suggests protection on its own. A nonce shows that a WordPress-generated token accompanied the request; it does not establish that the visitor may perform the operation, that submitted values are valid, or that malicious script is absent. Secure handling combines the nonce with a capability check, strict validation, and careful output escaping.

This guide builds a protected admin form from rendering to submission, then covers action names, AJAX, REST, caching, and testing. The examples suit plugins used by businesses in Sydney, Melbourne, Brisbane, and Perth, where maintainers may work across states while administrators handle customer information under Australian rules.

What the Nonce Field Really Does

WordPress derives each nonce from a named action, the current user and session, and a time-based tick. wp_nonce_field() calls wp_create_nonce() to generate a value and places it in a hidden input. By default, it also emits a referer field, although the nonce remains the primary check because referer information can be absent or changed. The first argument selects the action and the second selects the field name.

A token is normally usable for 12 to 24 hours, depending on where the current time falls within WordPress’s nonce tick. It is not a one-use token: the same value can be submitted more than once while it remains valid. Regenerating a form on every request therefore does not make the nonce replay-resistant.

The protection addresses CSRF, in which another site causes a browser to send a request with the visitor’s cookies. It does not stop cross-site scripting, stolen credentials, or a logged-in user from exceeding their permissions. Those risks require capability checks, input validation, output escaping, and ordinary WordPress security controls.

Choosing Stable Actions and Field Names

Use an action that names the exact operation, such as my_plugin_save_settings, and keep it stable between the form and handler. A token generated for saving settings must not validate an import, delete, or other endpoint. Prefix actions with the plugin slug so another plugin cannot accidentally share the same scope.

The field name controls where the token travels in the request. wp_nonce_field() defaults to _wpnonce, but a plugin-specific name such as my_plugin_settings_nonce makes handlers easier to audit and avoids collisions. The same name must be passed to the verification call; the action and field name serve different purposes.

Keep the action server-controlled. For an item-specific operation, derive both creation and verification values from a trusted record ID rather than accepting the action string from $_POST. On multisite, include the relevant site or object context when operations span networks, then check capabilities against the active site.

Treat nonce values as temporary secrets. Do not log them, send them to analytics services, place them in support screenshots, or use them as API credentials. A nonce is scoped protection for a WordPress request, not evidence of identity beyond the session that generated it.

Adding a Nonce to a Plugin Form

The form should use POST and a server endpoint such as admin-post.php. Include a hidden action value so WordPress can route the request to the matching admin_post_ hook. Render the page only to users who hold the required capability, and place the nonce field inside the form so it is submitted with the operation.

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="my_plugin_save_settings">

    <label for="my-plugin-support-email">Support email</label>
    <input
        type="email"
        id="my-plugin-support-email"
        name="my_plugin_support_email"
        value="<?php echo esc_attr( get_option( 'my_plugin_support_email', '' ) ); ?>"
    >

    <?php wp_nonce_field( 'my_plugin_save_settings', 'my_plugin_settings_nonce' ); ?>
    <?php submit_button( 'Save settings' ); ?>
</form>

wp_nonce_field() prints a hidden input and returns the same markup, so a template can call it directly as shown. If its output is stored in a variable, print that returned value only in the intended form. Never accept the nonce through a GET query string, where it can be copied into logs, browser history, or analytics data.

Using admin-post.php keeps routing out of public query variables and works well for conventional plugin forms. The admin_post_my_plugin_save_settings hook is for authenticated requests. A separate admin_post_nopriv_ hook is required for anonymous handling, and guest workflows need extra care because an anonymous WordPress nonce can be shared by every logged-out visitor during the same tick.

Verifying the Submitted Nonce

On the receiving side, retrieve the expected field without trusting its shape. Unslash the raw value, sanitise it as text, and compare it with wp_verify_nonce() using the exact action used to create it. Return a 403 response for a missing or invalid token, and do not change any data before that check succeeds.

add_action( 'admin_post_my_plugin_save_settings', 'my_plugin_save_settings' );

function my_plugin_save_settings() {
    $raw_nonce = $_POST['my_plugin_settings_nonce'] ?? '';

    if ( ! is_string( $raw_nonce ) ) {
        wp_die(
            'The security token is missing.',
            'Invalid form request',
            array( 'response' => 403 )
        );
    }

    $nonce = sanitize_text_field( wp_unslash( $raw_nonce ) );

    if ( ! wp_verify_nonce( $nonce, 'my_plugin_save_settings' ) ) {
        wp_die(
            'The security token is missing or invalid.',
            'Invalid form request',
            array( 'response' => 403 )
        );
    }

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die(
            'You are not allowed to change these settings.',
            'Access denied',
            array( 'response' => 403 )
        );
    }

    $raw_email = $_POST['my_plugin_support_email'] ?? '';
    $email = is_string( $raw_email )
        ? sanitize_email( wp_unslash( $raw_email ) )
        : '';

    if ( ! is_email( $email ) ) {
        wp_die(
            'Enter a valid support email address.',
            'Invalid settings',
            array( 'response' => 400 )
        );
    }

    update_option( 'my_plugin_support_email', $email );

    wp_safe_redirect(
        add_query_arg(
            'updated',
            '1',
            admin_url( 'options-general.php?page=my-plugin' )
        )
    );
    exit;
}

wp_verify_nonce() returns 1 for a token from the current tick and 2 for one from the previous tick that is still accepted. Both are valid. The action must match exactly, including its prefix; otherwise a legitimate token for another operation is correctly rejected.

For a standard admin form, check_admin_referer( 'my_plugin_save_settings', 'my_plugin_settings_nonce' ) provides a compact alternative because it verifies and stops the request on failure. Use wp_verify_nonce() when the handler needs to return a custom JSON error or apply its own response policy.

Adding Authorisation and Input Checks

Nonce validation answers whether the request carries a plausible token. It does not answer whether that user may change the selected resource. Keep current_user_can() beside the nonce check and use the narrowest capability that fits the job. A plugin-specific capability, granted during activation, is usually better than manage_options for non-administrator roles.

For object-level work, pass the object ID to a mapped capability such as current_user_can( 'edit_post', $post_id ). Derive that ID from a trusted database lookup where possible. The nonce scopes the operation; the capability scopes the person and resource, so both checks remain necessary.

Treat submitted values as untyped input until validation has established their shape, then sanitise them for storage. Use sanitize_text_field(), sanitize_email(), integer casting, and allowlists according to each field. Sanitisation is not validation: sanitize_email() may alter malformed input rather than prove an address exists. Reject invalid input when the original meaning matters. Escape every value again when rendering it, use prepared SQL through $wpdb->prepare(), and never store or echo raw request data.

Supporting AJAX, REST, and Guest Requests

AJAX requests need the same nonce discipline. Pass a value from wp_create_nonce( 'my_plugin_import' ) to JavaScript with wp_localize_script(), include it in the request, and call check_ajax_referer( 'my_plugin_import', 'nonce', false ) on the server. The false argument lets the handler send a JSON error instead of an HTML failure page. Add the required capability check before processing the import.

REST endpoints use their own flow. For cookie-authenticated requests, send a value from wp_create_nonce( 'wp_rest' ) in the X-WP-Nonce header and enforce access in the route’s permission_callback. wp_nonce_field() is not a substitute for that header, REST authentication, or endpoint permissions. Application passwords and other authentication methods follow their own rules. Do not disable core REST cookie-authentication protections merely to make a request work; unauthenticated routes still need explicit permission callbacks.

Anonymous forms deserve particular caution. WordPress nonces for logged-out visitors can have a shared identity during a tick, so they do not by themselves provide strong protection for sensitive guest actions. Require a login for privileged work, or combine an anonymous workflow with a server-side session, a one-time challenge, rate limiting, and strict origin controls.

Page caching can also undermine otherwise correct code by serving one visitor’s nonce to another or by retaining an expired token. Exclude nonce-bearing pages from full-page caching, or obtain a fresh token through an uncached request. Check cached inline scripts too, since a localised AJAX nonce can become stale even when the surrounding page looks current.

Testing and Hardening the Plugin

Test the form as both an authorised and an unauthorised visitor. A valid submission with the right capability should succeed; missing, altered, and expired tokens should fail with a 403 response and leave stored data untouched. Also test a signed-in user without the required capability, a logged-out request, a malformed field value, and a token copied from another action.

Common failures usually point to a small mismatch. Check that the form action, verification action, and field name agree; confirm the visitor is still logged into the same session; and look for aggressive page caching. Logging out or changing sessions invalidates some nonce context, while a token generated for another browser cannot stand in for the current user’s token.

Security controls do not replace privacy obligations. For covered Australian businesses, the Privacy Act 1988 and Australian Privacy Principles govern how personal information is collected, used, disclosed, and secured. Plugins should support data minimisation, defined retention, secure disclosure to processors, and accurate privacy notices. Avoid placing submitted values or nonce tokens in debug logs, and document what the plugin collects.

Australian market details can expose unsafe assumptions in an otherwise working plugin. WooCommerce stores commonly calculate orders in Australian dollars with 10% GST, so calculate prices and tax on the server through WooCommerce APIs rather than trusting posted totals. Teams in Sydney, Melbourne, Brisbane, and Perth should store logs in UTC and convert for local display—AEST/AEDT, AEST, and AWST as appropriate—because nonce ticks do not change with daylight saving. Documentation can use Australian spellings such as “authorise” and “sanitise”, and a completed review might be described in plain local language as “no dramas”. The finished handler then has a clear boundary: the nonce rejects forged requests, the capability limits authority, validation protects data, and escaping keeps output safe.