~/teardowns/TD-032.md
←Back to teardowns
# TD-032CriticalSecurity

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)

my-plugin.phpphp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// my-plugin.php
add_action('wp_ajax_nopriv_update_profile', 'handle_profile_update');
function handle_profile_update() {
global $wpdb;
$user_id = intval($_POST['user_id']);
$email = $_POST['email'];
$wpdb->update($wpdb->users, ['user_email' => $email], ['ID' => $user_id]);
wp_send_json_success();
}
?>

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 →

Sources

Is your codebase vulnerable to this?

Run Diagnostic on My Project →