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.