← All field notes
Security

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 ↗.

More field notes

AI BehaviorHidden State Drift in your dev loop: why the model forgets your codebase mid-sessionWorkflowFrom chat to committed: a repeatable AI WordPress workflowWorkflowPrompt-to-Plugin: shipping a working WordPress plugin from a single spec