The security cost of AI-written plugins: the gaps models skip
Nonces, capability checks, escaping, prepared statements — the four guards LLMs omit because the happy path works without them.
An AI-written plugin optimizes for one thing: producing code that runs. Security guards don't make code run — they make it fail safely under inputs the model never tested — so they're exactly what gets dropped. The gaps are predictable, which is good news: a fixed checklist catches almost all of them.
Capability checks: "logged in" is not "allowed"
Models routinely gate an action on whether a user is logged in and stop there. But a subscriber is logged in too. Every state-changing action needs an explicit current_user_can() against the right capability, not a vague authentication check. The correct guard names the permission the action actually requires.
Nonces: proving intent, not identity
A capability check confirms who; a nonce confirms the request was intended from your form, blocking CSRF. LLMs treat nonces as optional decoration and omit them from AJAX and admin-post handlers by default. They belong on every form and every privileged AJAX action, verified server-side before anything else runs.
REST routes are the blind spot
The most dangerous gap is the custom REST route with a permission_callback of '__return_true' — or worse, omitted, which newer WordPress rejects but older habits still produce. A model will happily expose a data-mutating endpoint to the entire internet. The callback is not optional:
register_rest_route( 'wpds/v1', '/project/(?P<id>\d+)', array(
'methods' => 'POST',
'callback' => 'wpds_update_project',
'permission_callback' => function () {
return current_user_can( 'edit_posts' );
},
'args' => array(
'id' => array( 'sanitize_callback' => 'absint' ),
),
) );
Sanitize in, escape out, prepare always
The last gap is the data itself. Input from $_POST, $_GET, or a REST body is hostile until sanitized (sanitize_text_field, absint, sanitize_email). Output into HTML is a vector until escaped (esc_html, esc_attr, esc_url, wp_kses_post). Anything reaching $wpdb goes through prepare(). Models know these functions exist; they just don't reach for them under deadline, and neither do the developers who paste the output unread.
None of this is exotic — it's the WordPress plugin security baseline, unchanged for a decade. What's new is the volume: AI lets one developer generate ten plugins' worth of code, and ten times the surface for the same four omissions. Automate the checklist with PHPCS security sniffs so the guard scales with the generation.
Why models drift
Half of "the AI wrote bad code" is really the model losing track of your constraints mid-session. That failure mode has a name and a research home: Hidden State Drift ↗.