Introduction: The AI Promise vs. WordPress Reality
Leveraging large language models (LLMs) like GPT-4, Claude 3.5 Sonnet, or Gemini Pro to write WordPress plugin code has become a common practice for developers and site owners alike. The promise is incredibly appealing: describe the required functionality in plain English, and the AI generates a ready-to-use PHP file in seconds.
The problem, however, is that language models excel at syntax and structure but lack operational context and a real understanding of security principles within the WordPress ecosystem. Trained on massive datasets of public code, they inherit millions of outdated, insecure, and poorly written plugins published over the past two decades. As a result, AI often outputs code that is syntactically flawless and functional, but riddled with critical security vulnerabilities under the hood.
As WordPress engineers, we must adopt a strict “Zero Trust” posture toward any code written by an AI. This guide breaks down the five most common security failure patterns we observe during audits of AI-generated plugin code, illustrating them with vulnerable PHP snippets alongside secure, production-grade refactors conforming to the WordPress Coding Standards (WPCS).
1. Misusing is_admin() for Access Control
Perhaps the most common logical error made by AI when attempting to secure an endpoint is relying on the is_admin() function to restrict access to administrators.
Why It Fails
Despite its name, is_admin() does not verify whether the currently logged-in user is an administrator. It only checks whether the current request is for an administration page (such as any URL starting with /wp-admin/).
Every AJAX request sent to /wp-admin/admin-ajax.php — whether initiated by an administrator, a logged-in subscriber with minimal capabilities, or an unauthenticated anonymous visitor — will cause is_admin() to return true.
Vulnerable AI-Generated Code:
// An AI-generated hook to save plugin settings
add_action( 'admin_init', 'ai_save_plugin_settings' );
function ai_save_plugin_settings() {
// AI incorrectly assumes this restricts access to administrators
if ( is_admin() ) {
if ( isset( $_POST['my_plugin_option'] ) ) {
update_option( 'my_plugin_option', $_POST['my_plugin_option'] );
}
}
}
Secured Code:
To verify the actual permissions of the logged-in user, you must use the current_user_can() function, passing in a specific user capability rather than a role name, such as manage_options.
add_action( 'admin_init', 'secure_save_plugin_settings' );
function secure_save_plugin_settings() {
// 1. Verify the current user's actual capabilities
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have permission to execute this action.', 'secure-plugin' ) );
}
if ( isset( $_POST['my_plugin_option'] ) ) {
// Apply input sanitization (see Section 4)
$sanitized_value = sanitize_text_field( wp_unslash( $_POST['my_plugin_option'] ) );
update_option( 'my_plugin_option', $sanitized_value );
}
}
2. Completely Omitting Nonce Verification (CSRF)
Cross-Site Request Forgery (CSRF) attacks trick an authenticated user’s browser (e.g., an administrator) into executing unauthorized actions on a site they are currently logged into. WordPress uses cryptographic tokens called nonces to protect against CSRF.
AI generators frequently omit nonce creation and verification in admin forms, AJAX handlers, and custom REST API endpoints. This occurs because implementing nonces requires coordinating the front-end rendering layer (the HTML form containing the nonce field) with the back-end processing controller, which is difficult for LLMs to align across disconnected code generations.
Vulnerable AI-Generated AJAX Handler:
// AI-generated AJAX endpoint registration
add_action( 'wp_ajax_ai_delete_post', 'ai_delete_post_handler' );
function ai_delete_post_handler() {
// AI checks capabilities but completely ignores CSRF (no nonce check)
if ( ! current_user_can( 'delete_posts' ) ) {
wp_send_json_error( 'Unauthorized.' );
}
$post_id = intval( $_POST['post_id'] );
wp_delete_post( $post_id );
wp_send_json_success( 'Post deleted.' );
}
Secured Code:
The output form must output a nonce field using wp_nonce_field(), and the receiving PHP controller must verify the token using check_ajax_referer() or wp_verify_nonce().
// In the form rendering file or javascript payload:
// wp_nonce_field( 'delete_post_action', 'my_nonce_field' );
// Secured AJAX handler registration
add_action( 'wp_ajax_secure_delete_post', 'secure_delete_post_handler' );
function secure_delete_post_handler() {
// 1. Verify CSRF token early
check_ajax_referer( 'delete_post_action', 'my_nonce_field' );
// 2. Verify user capabilities
if ( ! current_user_can( 'delete_posts' ) ) {
wp_send_json_error( 'Unauthorized.' );
}
// 3. Validate and cast input variables
if ( ! isset( $_POST['post_id'] ) ) {
wp_send_json_error( 'Missing post ID.' );
}
$post_id = absint( $_POST['post_id'] );
if ( wp_delete_post( $post_id ) ) {
wp_send_json_success( 'Post successfully deleted.' );
} else {
wp_send_json_error( 'Error deleting post.' );
}
}
3. SQL Injection Vulnerabilities in $wpdb Queries
When a plugin interacts with the database directly rather than utilizing the native WP_Query wrapper, it uses the $wpdb global object. Unfortunately, LLMs frequently construct raw SQL strings using direct concatenation of user-supplied variables ($_POST, $_GET) instead of parameterizing the query.
Why It Is Dangerous
Concatenating raw variables directly into database queries allows an attacker to manipulate the query structure. This can lead to unauthorized data retrieval (such as hashed user passwords), database deletion, or administrative privilege escalation.
Vulnerable AI-Generated Query:
// User query function generated by AI
function ai_find_users_by_city( $city_name ) {
global $wpdb;
// Critical failure: direct concatenation of query variable
$query = "SELECT * FROM {$wpdb->prefix}custom_users WHERE city = '" . $city_name . "'";
return $wpdb->get_results( $query );
}
Secured Code:
No raw variables should ever be written directly into SQL strings. Instead, use the $wpdb->prepare() method, which functions as a parameterized query engine, casting values and escaping strings using format specifiers (%s for strings, %d for integers, %f for floats).
function secure_find_users_by_city( $city_name ) {
global $wpdb;
// 1. Use prepare() with format specifiers
$query = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}custom_users WHERE city = %s",
$city_name
);
return $wpdb->get_results( $query );
}
Note: If the $city_name variable comes directly from user input, it must still be sanitized using sanitize_text_field() before database processing.
4. Lack of Input Sanitization and Output Escaping (XSS)
Cross-Site Scripting (XSS) is one of the most common vulnerabilities found in WordPress plugins. It allows malicious JavaScript code to be stored in the database or reflected dynamically, executing in the browsers of unsuspecting visitors.
AI models often confuse sanitization (cleaning data on the way in) with escaping (securing data on the way out). Even if the AI sanitizes inputs, it often displays variables in templates without escaping them first, leaving the site open to Stored XSS.
Vulnerable AI-Generated Rendering:
// Feedback display loop written by AI
function ai_display_user_feedback() {
$feedbacks = get_option( 'ai_user_feedback_list', [] );
echo '<div class="feedback-list">';
foreach ( $feedbacks as $feedback ) {
// Critical failure: raw output without escaping
echo '<p class="feedback-item">' . $feedback['user_comment'] . '</p>';
}
echo '</div>';
}
Secured Code:
Follow the fundamental WordPress rule: Sanitize Early, Escape Late (sanitize input variables immediately; escape output immediately before outputting to the browser).
Choose the correct escaping function for the context:
esc_html()— for standard text nodes inside HTML elements.esc_attr()— for outputting inside HTML attribute values.esc_url()— for outputting links.wp_kses()— when you must allow specific, safe HTML tags (such as strong, anchors).
function secure_display_user_feedback() {
$feedbacks = get_option( 'secure_user_feedback_list', [] );
echo '<div class="feedback-list">';
foreach ( $feedbacks as $feedback ) {
$comment = isset( $feedback['user_comment'] ) ? $feedback['user_comment'] : '';
// 1. Escape output contextually immediately before rendering
echo '<p class="feedback-item">';
echo esc_html( $comment );
echo '</p>';
}
echo '</div>';
}
5. Dangerous File Upload Implementation (Arbitrary File Upload)
Allowing users to upload files (such as profile avatars or contact form attachments) is one of the highest-risk operations on a website. A mistake here allows attackers to upload a .php file, resulting in Remote Code Execution (RCE) and total server compromise.
AI-generated file handlers frequently rely on native PHP commands like move_uploaded_file() combined with weak file extension checks (such as simple string matching), which are trivial for attackers to bypass (e.g., uploading files named malicious.php.jpg or using alternate extensions like .phtml).
Vulnerable AI-Generated File Upload:
// Upload handler written by AI
function ai_handle_avatar_upload() {
if ( isset( $_FILES['avatar'] ) ) {
$file_name = $_FILES['avatar']['name'];
$ext = pathinfo( $file_name, PATHINFO_EXTENSION );
// AI assumes this extension check is safe
if ( in_array( $ext, ['jpg', 'jpeg', 'png'] ) ) {
$target = wp_upload_dir()['path'] . '/' . $file_name;
move_uploaded_file( $_FILES['avatar']['tmp_name'], $target );
}
}
}
Secured Code:
Never use native PHP move_uploaded_file() inside WordPress. Instead, use the core wp_handle_upload() function. It performs robust MIME type checking (verifying the file’s actual signature, not just its name), validates the upload status, and integrates with the WordPress permissions system.
function secure_handle_avatar_upload() {
// 1. Validate the CSRF token
if ( ! isset( $_POST['avatar_upload_nonce'] ) || ! wp_verify_nonce( $_POST['avatar_upload_nonce'], 'upload_avatar_action' ) ) {
wp_die( esc_html__( 'Invalid security token.', 'secure-plugin' ) );
}
// 2. Verify capability checks
if ( ! current_user_can( 'upload_files' ) ) {
wp_die( esc_html__( 'You do not have permission to upload files.', 'secure-plugin' ) );
}
if ( isset( $_FILES['avatar'] ) ) {
if ( ! function_exists( 'wp_handle_upload' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
// 3. Force-restrict allowed MIME types
$allowed_mimes = [
'jpg|jpeg' => 'image/jpeg',
'png' => 'image/png'
];
$upload_overrides = [
'test_form' => false,
'mimes' => $allowed_mimes
];
// 4. Delegate upload processing to core function
$movefile = wp_handle_upload( $_FILES['avatar'], $upload_overrides );
if ( $movefile && ! isset( $movefile['error'] ) ) {
$avatar_url = $movefile['url'];
update_user_meta( get_current_user_id(), 'user_avatar', $avatar_url );
} else {
// Handle error output securely
error_log( 'File upload failure: ' . $movefile['error'] );
}
}
}
Audit Methodology: Implementing Zero Trust for AI Code
To safely deploy AI-generated code, your engineering team must establish a systematic review process. Implement these three steps before pushing any generated assets to production:
Step 1: Automate Static Analysis (PHPCS)
Configure WordPress Coding Standards (WPCS) for the PHP_CodeSniffer tool. It scans files and flags missing sanitization, omitted nonces, and unescaped variables automatically.
# Scan a generated plugin file using WordPress standards
vendor/bin/phpcs --standard=WordPress-Extra path/to/generated-plugin.php
Step 2: Conduct a Manual Code Review
Review every handler executing HTTP queries, database transactions, AJAX calls, or REST API connections against the following flowchart:
graph TD
A["Receive AI-Written Plugin Code"] --> B{"Does it accept user input?"}
B -- Yes --> C["Check for Nonce & Capability Checks"]
B -- No --> D{"Does it modify the database?"}
C --> C1["Implement check_ajax_referer() / CSRF validation"]
C --> C2["Implement current_user_can() permissions check"]
C1 --> D
C2 --> D
D -- Yes --> E["Check for $wpdb->prepare() usage"]
D -- No --> F{"Does it output to the browser?"}
E --> E1["Implement parameterized SQL variables"]
E1 --> F
F -- Yes --> G["Verify esc_html(), esc_attr(), or esc_url()"]
F -- No --> H["Proceed to integration testing"]
G --> G1["Implement contextual output escaping"]
G1 --> H
Step 3: Run Low-Privilege Tests
Verify the code’s access boundaries. Log in as a user with the “Subscriber” role and trigger the plugin’s REST or AJAX actions directly via curl or Postman. If the server executes modifications or reveals database data, the permission layers are missing.
Summary and Recommendations
Artificial intelligence is a powerful accelerator for writing code, but it cannot assume engineering responsibility. Treat all AI-written code as though it was penned by an inexperienced intern.
Ensure you implement robust capability checks (current_user_can()), CSRF verification (wp_verify_nonce()), database parameterization ($wpdb->prepare()), input sanitization, and late output escaping. By combining these core practices with static analysis tooling, you can write WordPress code faster while maintaining a completely secure application.







