Code

Secure a custom form submission handler against CSRF using nonces

A contact-request form scenario reasoning through nonce lifetime for logged-out visitors, combining nonces with capability checks, and safe redirect handling.

Works with Claude / GPT1,750 uses 4.5

The prompt

code-csrf-nonce-form-handler
I have a custom "Request a Callback" form on the front end, processed via
`admin-post.php`, that currently has no CSRF protection at all:

```php
add_action('admin_post_nopriv_request_callback', 'myplugin_handle_callback_request');
add_action('admin_post_request_callback', 'myplugin_handle_callback_request');
```

CONTEXT: [this form is submitted by both logged-out visitors and logged-in customers,
a security review flagged the missing nonce, and the form page can be cached by a page
caching plugin for up to 24 hours]

Secure this like a senior dev handling CSRF properly, in order:
1. Add `wp_nonce_field('myplugin_callback_request', 'myplugin_nonce')` to the form and
   verify it in the handler with `wp_verify_nonce()`, checked against both possible
   return values (`1` and `2` both mean valid, `false` means invalid, and treating only
   `1` as valid is a subtle bug that rejects legitimate nonces past their first
   12-hour tick).
2. Flag the caching interaction explicitly: since the form page can be cached for up to
   24 hours, and a WordPress nonce is only valid for roughly 24 hours from creation
   (tied to the session token, not just a fixed timer), a visitor loading a
   23-hour-stale cached page and submitting the form could get a "nonce expired,
   please refresh" failure. Handle this gracefully: show the visitor a friendly
   "session expired, please try again" message rather than a raw
   `wp_die('Nonce verification failed')`, and consider excluding this specific form
   page from full-page caching if that's an option, since nonce-protected forms and
   aggressive page caching are fundamentally in tension.
3. Explain why a nonce alone isn't the complete picture: for the logged-out submission
   path in particular, also add a honeypot field and a basic rate limit (transient
   keyed by IP), since a nonce only proves the request came from a page your site
   served, it doesn't prevent a scraper from loading the form fresh each time and
   submitting via script.
4. Use `wp_safe_redirect()` (not `wp_redirect()`) after processing, validated against
   `wp_validate_redirect()` if the redirect destination comes from a `_wp_http_referer`
   or similar user-influenced field, to avoid turning the fix into an open-redirect
   vector while patching the CSRF issue.
5. Note the edge case for logged-in customers: since their session is also tied to
   their auth cookie, confirm the nonce check still passes correctly after a customer
   logs in mid-session on a different tab (nonce values differ per logged-in vs
   logged-out state), and test the logged-in submission path separately rather than
   assuming the logged-out fix covers both.

Replace the form fields, hook names, and caching setup with your actual scenario.

The wp_verify_nonce() return value check in step 1 is a genuinely common bug: treating anything other than exactly 1 as failure silently breaks the form for a chunk of legitimate users once their nonce ages past its first half-life, which looks like an intermittent, unreproducible failure until you check the actual return values being compared.