The AJAX Endpoint That Never Asked Who You Are
A WordPress profile update open to the entire internet
Published Oct 29, 2026
Executive Summary
A vibe-coded WordPress plugin registers its profile-update handler on wp_ajax_nopriv_ — the hook that runs for users who are NOT logged in — and skips both the nonce check and the capability check. The result: an unauthenticated visitor can POST a user ID and a new email address and rewrite any account on the site, then trigger a password reset to take it over.
The Prompt
“Add an AJAX endpoint to my WordPress plugin that updates the logged-in user's profile.”
The Flaw
wp_ajax_nopriv_* hooks fire for visitors who are NOT logged in, so this endpoint is reachable by anyone on the internet. There is no check_ajax_referer() (nonce) and no current_user_can() (capability) — and worse, the user ID is read from the POST body instead of the session. An attacker can send user_id=1 with a new email, rewrite the admin account's address, then request a password reset and take the account over.
Risk & Impact
[Security] Any unauthenticated visitor can change any account's email address — including administrators — by POSTing a user ID. The endpoint waits, undetected, until someone probes it.
Cost: Full account takeover and site compromise, with no login required.
Code Comparison
❌ Vulnerable (AI generated)
❌ Vulnerable (AI generated)
✅ Hardened (production ready)
The Fix
Register on wp_ajax_ (logged-in only), verify a nonce with check_ajax_referer(), authorize with current_user_can(), and take the identity from the session — never from the request body.
Prevention
- [linter] WordPress coding-standard and security sniffs (e.g. PHPCS with WordPress.Security.NonceVerification and WordPress.WP.Capabilities) flag AJAX handlers missing nonce and capability checks.
- [ci] A security smoke test that fires the endpoint unauthenticated (no nonce, no cookies) must get a 403 — fail the build if it succeeds.
- [manual] Tell the agent explicitly: 'use wp_ajax_, never wp_ajax_nopriv_; check the nonce and the capability; take the user ID from the session.' (rules 3 and 6)
Why This Happened
Root cause: the prompt omitted Rule 3, Rule 6, and Rule 11 — the guardrails that would have caught this. See the full guide →
