Auditing AI-generated WordPress plugin code
EN

Auditing AI-generated WordPress plugin code

Last verified: July 10, 2026
8 min read
Guide
500+ WP projects
Security auditor

#Why AI plugin code fails review

Large language models can draft WordPress plugin PHP in seconds. The output often passes a smoke test in wp-admin and still fails a security review because the model optimises for syntax, not for WordPress execution context. In rescue engagements we see the same five gaps repeat: missing capability checks, absent nonce verification, unsanitised $_POST, concatenated SQL, and PHP files without an ABSPATH guard. This guide documents those patterns with anonymised snippets from real audits. The examples are deliberately broken teaching samples, not runnable exploits.

If your site was built or heavily modified by an AI assistant, these checks sit inside the broader AI-built website rescue workflow. When the custom code surface is large or you need a formal pass on the whole stack, pair this review with a WordPress security audit.


#1. Missing capability checks (and misusing is_admin())

The most common access-control mistake in generated code is calling is_admin() and assuming only administrators can reach the handler. is_admin() only checks whether the current request targets the administration area. Every call to /wp-admin/admin-ajax.php satisfies that condition, including requests from subscribers and unauthenticated visitors.

#Bad example from an anonymised settings handler

add_action( 'admin_init', 'acme_save_plugin_settings' );
function acme_save_plugin_settings() {
    if ( is_admin() && isset( $_POST['acme_option'] ) ) {
        update_option( 'acme_option', $_POST['acme_option'] );
    }
}

A subscriber who can load any front-end page can POST to admin-ajax.php or trigger admin_init flows that were never meant for their role. The fix is current_user_can( 'manage_options' ) (or a narrower capability) before any write.

#What we look for in review

  • Handlers registered on admin_init, wp_ajax_*, or REST routes without current_user_can().
  • Role name checks (if ( $user->roles[0] === 'administrator' )) instead of capabilities.
  • is_admin() used as the only gate on mutating operations.

#2. Missing wp_nonce verification (CSRF)

Cross-site request forgery tricks a logged-in administrator’s browser into submitting a request the user did not intend. WordPress mitigates this with nonces. AI-generated AJAX handlers often check capabilities but skip wp_verify_nonce() or check_ajax_referer(), because the model generated the handler and the form in separate passes.

#Bad example from an anonymised delete handler

add_action( 'wp_ajax_acme_delete_entry', 'acme_delete_entry_handler' );
function acme_delete_entry_handler() {
    if ( ! current_user_can( 'delete_posts' ) ) {
        wp_send_json_error( 'Unauthorized.' );
    }
    $entry_id = intval( $_POST['entry_id'] );
    wp_delete_post( $entry_id, true );
    wp_send_json_success( 'Deleted.' );
}

An administrator’s session is enough for an attacker to forge a POST if no nonce is required. Capability checks alone do not stop CSRF.

#What we look for in review

  • Any $_POST / $_GET mutation without wp_verify_nonce() or check_ajax_referer().
  • REST routes with permission_callback set to __return_true or omitted.
  • Admin forms rendered without wp_nonce_field().

#3. Unsanitised $_POST written to the database or echoed to the page

AI output frequently moves $_POST values into update_option(), update_post_meta(), or straight into echo without sanitize_* or wp_unslash(). That opens stored XSS and option-poisoning paths even when the handler is admin-only.

#Bad example from an anonymised feedback saver

add_action( 'wp_ajax_acme_save_feedback', 'acme_save_feedback' );
function acme_save_feedback() {
    $comment = $_POST['user_comment'];
    $list = get_option( 'acme_feedback_list', array() );
    $list[] = array( 'user_comment' => $comment );
    update_option( 'acme_feedback_list', $list );
    wp_send_json_success();
}

No nonce, no capability check, no sanitisation. A later template that echoes $feedback['user_comment'] without esc_html() completes the stored XSS chain.

#What we look for in review

  • $_POST, $_GET, or $_REQUEST reaching persistence without sanitize_text_field(), absint(), or context-appropriate sanitizers.
  • Output in templates without esc_html(), esc_attr(), or esc_url() as appropriate.
  • Missing wp_unslash() before sanitisation on form data.

#4. SQL concatenation instead of $wpdb->prepare()

When generated code touches custom tables, models often build SQL with string concatenation. The pattern looks correct to a non-PHP reviewer because the query “works” in testing.

#Bad example from an anonymised lookup function

function acme_find_records_by_ref( $ref_code ) {
    global $wpdb;
    $sql = "SELECT * FROM {$wpdb->prefix}acme_records WHERE ref_code = '" . $ref_code . "'";
    return $wpdb->get_results( $sql );
}

If $ref_code ever comes from request input without preparation, the query structure is attacker-controlled.

#What we look for in review

  • Variables interpolated inside double-quoted SQL strings.
  • $wpdb->query() / get_results() without a preceding $wpdb->prepare().
  • Correct prepare() usage but % literals not escaped when needed.

#5. Direct file access without an ABSPATH guard

Every PHP file in a plugin or theme should refuse execution when WordPress has not bootstrapped. Generated snippets often omit the guard, especially in “helper” files the model split out for readability.

#Bad example from an anonymised include file

<?php
// helpers.php - no bootstrap check
function acme_get_export_rows() {
    global $wpdb;
    return $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}acme_records" );
}

Requesting /wp-content/plugins/acme-exporter/helpers.php directly may run the file outside WordPress, bypassing authentication entirely depending on server configuration.

#Correct pattern

<?php
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

#What we look for in review

  • Any .php file in the plugin tree missing the guard (including uninstall.php and vendor-adjacent copies).
  • define( 'WP_USE_THEMES', false ); require 'wp-load.php'; patterns in generated “API” files without authentication.

#6. Hallucinated APIs and unverified dependencies

Generative models invent plausible function names: wp_verify_admin(), sanitize_all_input(), get_current_user_role(). They follow WordPress naming closely enough to survive a quick skim and fail on first execution. A separate risk is slopsquatting: a composer.json that references a package one character off a real library.

Cross-check every unfamiliar symbol against developer.wordpress.org. Verify Composer packages on Packagist before composer install. In rescue work this pattern often pushes a site from “fix one handler” to “rewrite the custom plugin” when hallucinations are scattered across files.


#Audit workflow we use before production

Treat AI-generated plugin code as untrusted input. The sequence below is what we run in AI-built website rescue engagements before any custom PHP ships to production.

#Step 1: Static analysis with PHPCS

Run WordPress Coding Standards against the generated tree. PHPCS flags many missing escapes, nonces, and sanitisation gaps automatically.

vendor/bin/phpcs --standard=WordPress-Extra path/to/generated-plugin/

#Step 2: Manual checklist on every mutating handler

For each AJAX action, REST route, form handler, and upload path:

  1. ABSPATH guard on every PHP file.
  2. Nonce verified before reads of $_POST / $_GET that change state.
  3. current_user_can() with the narrowest capability that fits.
  4. Input sanitised with wp_unslash() + appropriate sanitize_*.
  5. Database reads and writes through $wpdb->prepare() when SQL is dynamic.
  6. Output escaped at the point of render.

#Step 3: Low-privilege probing

Log in as a subscriber (or use no cookie) and replay the plugin’s AJAX and REST endpoints with curl or Postman. If state changes or private fields return, the permission model is incomplete. A full WordPress security audit extends this with CVE cross-checks on third-party plugins and server exposure review.

graph TD
    A["Receive AI-written plugin code"] --> B{"Mutates state or reads private data?"}
    B -- Yes --> C["ABSPATH guard on every PHP file"]
    C --> D["Nonce + capability on handler"]
    D --> E{"Touches database?"}
    B -- No --> F{"Outputs to HTML?"}
    E -- Yes --> G["$wpdb->prepare() for dynamic SQL"]
    E -- No --> F
    G --> F
    F -- Yes --> H["esc_html / esc_attr / esc_url at render"]
    F -- No --> I["Low-privilege curl tests"]
    H --> I

#When to fix the plugin vs rescue the whole site

Localised gaps (one AJAX handler, one admin form) are worth patching after review. When the same five patterns appear across multiple generated files, when hallucinated functions are wired into production paths, or when AI-chosen plugins carry live CVEs on top of bad custom code, stop patching in isolation. The AI-built website rescue audit produces an explicit keep / rewrite / remove split so you do not pay for a full rebuild when a rescue is enough, and you do not patch forever when the foundation is unsound. When the stack also carries thirty-plus plugins and broken Core Web Vitals, pair the code audit with Core Web Vitals rescue for AI-built sites.

Pricing for rescue and audit work is individual and scoped after that inventory. Contact us with the plugin or site URL and how the code was generated.


#Summary

AI accelerates drafting WordPress plugin PHP. It does not replace a security review. The failures that recur in our audits are predictable: missing capabilities, missing nonces, unsanitised input, concatenated SQL, and unguarded PHP files. Run PHPCS, walk the checklist on every handler, and probe with a low-privilege account before you trust vibe-coded plugins on a production store or membership site.

Next step

Turn the article into an actual implementation

This block strengthens internal linking and gives readers the most relevant next move instead of leaving them at a dead end.

Why is AI-generated WordPress plugin code often insecure?#
Large language models predict plausible PHP from public repositories, much of which predates modern WordPress security practice. The output is syntactically valid but often skips nonce verification, capability checks, sanitisation, and prepared statements because those require coordinated front-end and back-end context the model does not hold across generations.
What is the difference between is_admin() and current_user_can()?#
is_admin() returns true for any request routed through wp-admin, including admin-ajax.php calls from subscribers or anonymous visitors. current_user_can() checks whether the logged-in user has a specific capability such as manage_options. Access control must use capabilities, not is_admin().
How does AI introduce SQL injection in WordPress plugins?#
The common pattern is concatenating $_POST or $_GET values directly into a SQL string passed to $wpdb->query() or $wpdb->get_results(). WordPress expects dynamic values to go through $wpdb->prepare() with format specifiers (%s, %d, %f).
Why does every plugin PHP file need an ABSPATH guard?#
Without if ( ! defined( 'ABSPATH' ) ) { exit; } at the top of each file, an attacker can request the PHP file directly over HTTP. The script may execute outside WordPress, skipping authentication, nonces, and capability checks entirely.
When should I rescue a vibe-coded site instead of rewriting the plugin?#
If the failure patterns are localised (one handler missing a nonce, one query without prepare) and the rest of the stack is sound, a targeted fix after audit is enough. If hallucinated APIs, missing guards, and sprawl appear across multiple custom files, scope a broader rescue or rebuild. Our AI-built website rescue page walks through that triage.

Need an FAQ tailored to your industry and market? We can build one aligned with your business goals.

Let’s discuss

Related Articles