Agent skill

Wordpress Security

by duracelltomi in duracelltomi/gtm4wp

Guide to maintain creating modern and secure code while developing WordPress plugins.

GPL-2.0-or-laterAuto-check passedSecurity

Install Wordpress Security

skills CLI
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/wordpress-security .claude/skills/wordpress-security && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
wordpress-security
GitHub stars
174
Token cost
~7.2k tokens
SKILL.md length
586 words
Files
1
Skills in repo
6
Repo updated
First seen
Licence
GPL-2.0-or-later

At a glance

Guide to maintain creating modern and secure code while developing WordPress plugins.

  • Works in 11 steps: Input Sanitization → Output Escaping → CSRF Protection (Nonces) → …
  • Tasks that involve Web application vulnerabilities
  • SKILL.md covers Overview, 1. Input Sanitization, 2. Output Escaping and 3. CSRF Protection (Nonces), plus 2 more sections
  • Reaches api.vertex-ai.google.com; needs VERTEX_AI_API_KEY and AUTH_KEY

What it does

Wordpress Security is an agent skill from duracelltomi/gtm4wp. Guide to maintain creating modern and secure code while developing WordPress plugins.

Its SKILL.md is about 7.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Security, covering Web application vulnerabilities. It works with WordPress and WooCommerce. The repository describes itself as: Google Tag Manager plugin for WordPress. The licence is GPL-2.0-or-later.

When your agent uses it

  • Tasks that involve Web application vulnerabilities

Example prompts

  • “/wordpress-security”

Requirements

  • A credential in AUTH_KEY
  • A credential in SECURE_AUTH_KEY

Workflow steps

11 steps, taken from the step headings in SKILL.md.

  1. Input Sanitization
  2. Output Escaping
  3. CSRF Protection (Nonces)
  4. SQL Injection Prevention
  5. Capability & Permission Checks
  6. Encryption & Secrets Management
  7. File Upload Security
  8. External API Security
  9. Common Vulnerabilities to Avoid
  10. Security Checklist
  11. Additional Resources

What it can do on your machine

Read from SKILL.md and the folder at commit dfe20ee. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    No scripts in the folder and no shell commands in SKILL.md (its code samples are php).

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • api.vertex-ai.google.com

    Also links to:

    • developer.wordpress.org
    • github.com
    • woocommerce.com
    • wordpress.org

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • VERTEX_AI_API_KEY
    • AUTH_KEY
    • SECURE_AUTH_KEY
    • LOGGED_IN_KEY
    • NONCE_KEY
    • ENCRYPTION_KEY

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Wordpress Security loads about 7.2k tokens when it runs. Until then it costs about 26 tokens; SKILL.md has 586 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~26
When it runs · the whole SKILL.md, loaded when a task matches
~7.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check passed

The automated check found no risky patterns in SKILL.md.

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from duracelltomi/gtm4wp at commit dfe20ee, republished under its GPL-2.0-or-later licence (© duracelltomi). 586 words, ~7,162 tokens.

Download SKILL.mdSave it as .claude/skills/wordpress-security/SKILL.md (or your agent's skills folder).
name
wordpress-security
description
Guide to maintain creating modern and secure code while developing WordPress plugins.
license
GPL-2.0-or-later

WordPress Plugin Security Hardening Skill

Overview

Comprehensive security guidelines for WordPress plugin development, with emphasis on WooCommerce extensions and external API integrations.


1. Input Sanitization

Core Principle

NEVER trust user input. All data from users, URLs, forms, AJAX requests, or external APIs must be sanitized before processing or storage.

Sanitization Functions by Data Type
php
// Text input (strips tags, encodes special chars)
$clean_text = sanitize_text_field($_POST['field_name']);

// Textarea (allows line breaks, strips tags)
$clean_textarea = sanitize_textarea_field($_POST['description']);

// Email
$clean_email = sanitize_email($_POST['email']);

// URL
$clean_url = esc_url_raw($_POST['website']);

// File name
$clean_filename = sanitize_file_name($_FILES['upload']['name']);

// SQL LIKE query (escapes % and _)
$clean_search = $wpdb->esc_like($_POST['search_term']);

// HTML content (allows safe HTML tags)
$clean_html = wp_kses_post($_POST['rich_content']);

// Integer
$clean_id = absint($_POST['product_id']);

// Array of integers
$clean_ids = array_map('absint', $_POST['product_ids']);

// Boolean
$clean_bool = (bool) $_POST['is_enabled'];

// Alphanumeric only
$clean_code = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['code']);
WooCommerce-Specific Sanitization
php
// Product price
$price = wc_format_decimal($_POST['price']);

// Product stock quantity
$stock = wc_stock_amount($_POST['stock']);

// Clean product meta
$meta_value = wc_clean($_POST['custom_meta']);
Custom Sanitization Pattern
php
function sanitize_api_key($key) {
    // Remove whitespace
    $key = trim($key);
    
    // Allow only specific characters
    $key = preg_replace('/[^a-zA-Z0-9\-_]/', '', $key);
    
    // Validate length
    if (strlen($key) < 20 || strlen($key) > 100) {
        return new WP_Error('invalid_key', 'API key length invalid');
    }
    
    return $key;
}

2. Output Escaping

Core Principle

Escape late, escape often. Always escape data when outputting to HTML, JavaScript, URLs, or attributes.

Escaping Functions by Context
php
// HTML content
echo esc_html($user_provided_text);

// HTML attributes
echo '<input value="' . esc_attr($value) . '">';

// URL
echo '<a href="' . esc_url($link) . '">Link</a>';

// JavaScript
echo '<script>var message = "' . esc_js($message) . '";</script>';

// Textarea content
echo '<textarea>' . esc_textarea($content) . '</textarea>';

// Translation with variables (SECURE)
echo sprintf(
    esc_html__('Welcome, %s!', 'textdomain'),
    esc_html($username)
);

// Translation with HTML (use wp_kses_post)
echo wp_kses_post(
    sprintf(__('Click <a href="%s">here</a>', 'textdomain'), esc_url($url))
);
Admin UI Escaping
php
// Admin notices
echo '<div class="notice notice-success"><p>' . 
     esc_html__('Settings saved successfully.', 'textdomain') . 
     '</p></div>';

// Settings field
add_settings_field(
    'api_key',
    esc_html__('API Key', 'textdomain'),
    function($args) {
        $value = get_option('my_api_key');
        echo '<input type="text" name="my_api_key" value="' . 
             esc_attr($value) . '" class="regular-text">';
    }
);
JSON Output (AJAX)
php
// ALWAYS use wp_send_json functions
wp_send_json_success(array(
    'message' => 'Product updated',
    'product_id' => absint($product_id)
));

wp_send_json_error(array(
    'message' => esc_html__('Invalid product ID', 'textdomain')
));

// NEVER use echo or print with json_encode directly
// BAD: echo json_encode($data);

3. CSRF Protection (Nonces)

Core Principle

Verify intent. All state-changing operations must verify a nonce to prevent Cross-Site Request Forgery.

Form Nonces
php
// Create nonce in form
<form method="post" action="">
    <?php wp_nonce_field('save_product_settings', 'product_settings_nonce'); ?>
    <input type="text" name="product_name">
    <button type="submit">Save</button>
</form>

// Verify nonce when processing
if (!isset($_POST['product_settings_nonce']) || 
    !wp_verify_nonce($_POST['product_settings_nonce'], 'save_product_settings')) {
    wp_die(esc_html__('Security check failed', 'textdomain'));
}
URL Nonces
php
// Create nonce in URL
$delete_url = wp_nonce_url(
    admin_url('admin.php?action=delete_product&id=' . $product_id),
    'delete_product_' . $product_id
);

echo '<a href="' . esc_url($delete_url) . '">Delete</a>';

// Verify in handler
if (!isset($_GET['_wpnonce']) || 
    !wp_verify_nonce($_GET['_wpnonce'], 'delete_product_' . $product_id)) {
    wp_die(esc_html__('Security check failed', 'textdomain'));
}
AJAX Nonces
php
// Pass nonce to JavaScript
wp_localize_script('my-ajax-script', 'myAjax', array(
    'ajaxurl' => admin_url('admin-ajax.php'),
    'nonce' => wp_create_nonce('my_ajax_action')
));

// JavaScript (send with request)
jQuery.ajax({
    url: myAjax.ajaxurl,
    type: 'POST',
    data: {
        action: 'my_ajax_action',
        nonce: myAjax.nonce,
        product_id: productId
    },
    success: function(response) {
        console.log(response);
    }
});

// PHP (verify in AJAX handler)
add_action('wp_ajax_my_ajax_action', 'handle_my_ajax_action');

function handle_my_ajax_action() {
    // Verify nonce FIRST
    check_ajax_referer('my_ajax_action', 'nonce');
    
    // Verify capability
    if (!current_user_can('edit_products')) {
        wp_send_json_error('Insufficient permissions');
    }
    
    // Process request
    $product_id = absint($_POST['product_id']);
    
    // Return response
    wp_send_json_success(array('message' => 'Success'));
}
REST API Nonces
php
// For custom REST endpoints, use nonce middleware
register_rest_route('myplugin/v1', '/update-settings', array(
    'methods' => 'POST',
    'callback' => 'update_settings_callback',
    'permission_callback' => function() {
        return current_user_can('manage_options');
    }
));

// If accessed via JavaScript, send nonce in header
wp_localize_script('my-rest-script', 'wpApiSettings', array(
    'root' => esc_url_raw(rest_url()),
    'nonce' => wp_create_nonce('wp_rest')
));

// JavaScript
fetch(wpApiSettings.root + 'myplugin/v1/update-settings', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-WP-Nonce': wpApiSettings.nonce
    },
    body: JSON.stringify(data)
});

4. SQL Injection Prevention

Core Principle

ALWAYS use prepared statements. Never concatenate user input into SQL queries.

Basic Prepared Statements
php
global $wpdb;

// SELECT with placeholder
$product_id = absint($_GET['product_id']);
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}my_table WHERE product_id = %d",
    $product_id
));

// INSERT with placeholders
$wpdb->insert(
    $wpdb->prefix . 'my_table',
    array(
        'product_id' => $product_id,
        'api_key' => $api_key,
        'created_at' => current_time('mysql')
    ),
    array('%d', '%s', '%s') // format types
);

// UPDATE with placeholders
$wpdb->update(
    $wpdb->prefix . 'my_table',
    array('status' => 'active'),      // data
    array('product_id' => $product_id), // where
    array('%s'),                         // data format
    array('%d')                          // where format
);

// DELETE with placeholders
$wpdb->delete(
    $wpdb->prefix . 'my_table',
    array('product_id' => $product_id),
    array('%d')
);
Placeholder Types
php
// %d = integer
// %f = float
// %s = string

$wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}products 
     WHERE price > %f AND category = %s AND stock > %d",
    99.99,
    'electronics',
    10
);
IN Clause (Multiple Values)
php
// Sanitize array of IDs
$product_ids = array_map('absint', $_POST['product_ids']);

// Create placeholders
$placeholders = implode(',', array_fill(0, count($product_ids), '%d'));

// Use in query
$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}products WHERE id IN ($placeholders)",
    $product_ids
));
LIKE Queries
php
$search_term = sanitize_text_field($_GET['search']);

// Escape LIKE wildcards
$like_term = '%' . $wpdb->esc_like($search_term) . '%';

$results = $wpdb->get_results($wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}products WHERE name LIKE %s",
    $like_term
));
Never Do This (Vulnerable)
php
// NEVER EVER DO THIS - SQL INJECTION VULNERABILITY
$product_id = $_GET['product_id'];
$results = $wpdb->get_results(
    "SELECT * FROM {$wpdb->prefix}products WHERE id = $product_id"
);

// ALSO VULNERABLE
$search = $_GET['search'];
$results = $wpdb->get_results(
    "SELECT * FROM {$wpdb->prefix}products WHERE name LIKE '%$search%'"
);

5. Capability & Permission Checks

Core Principle

Check permissions early and often. Never assume a user has the right to perform an action.

Common Capabilities
php
// Admin access
if (!current_user_can('manage_options')) {
    wp_die(esc_html__('Unauthorized access', 'textdomain'));
}

// WooCommerce product management
if (!current_user_can('edit_products')) {
    wp_send_json_error('Insufficient permissions');
}

// Specific product editing
if (!current_user_can('edit_product', $product_id)) {
    wp_send_json_error('Cannot edit this product');
}

// Shop management
if (!current_user_can('manage_woocommerce')) {
    return;
}
Admin Menu/Page Protection
php
add_menu_page(
    'Plugin Settings',
    'My Plugin',
    'manage_options', // REQUIRED capability
    'my-plugin-settings',
    'render_settings_page'
);

function render_settings_page() {
    // Double-check capability
    if (!current_user_can('manage_options')) {
        wp_die(esc_html__('Unauthorized access', 'textdomain'));
    }
    
    // Render page
}
AJAX Handler Protection
php
add_action('wp_ajax_update_product', 'update_product_handler');

function update_product_handler() {
    // 1. Verify nonce
    check_ajax_referer('update_product_nonce', 'nonce');
    
    // 2. Check capability
    if (!current_user_can('edit_products')) {
        wp_send_json_error(array(
            'message' => esc_html__('Insufficient permissions', 'textdomain')
        ));
    }
    
    // 3. Check specific object permission
    $product_id = absint($_POST['product_id']);
    if (!current_user_can('edit_product', $product_id)) {
        wp_send_json_error(array(
            'message' => esc_html__('Cannot edit this product', 'textdomain')
        ));
    }
    
    // 4. Process request
    // ...
}
Custom Capabilities
php
// Add custom capability to role
$role = get_role('shop_manager');
$role->add_cap('manage_vertex_ai_settings');

// Check custom capability
if (!current_user_can('manage_vertex_ai_settings')) {
    wp_send_json_error('Unauthorized');
}

6. Encryption & Secrets Management

Core Principle

Never store sensitive data in plain text. Use WordPress salts for key derivation, encrypt at rest, protect in transit.

php
/**
 * Derive encryption key from WordPress salts using HKDF
 * This is SECURE - keys are derived, not stored
 */
function get_encryption_key() {
    // Use WordPress salts as key material
    $key_material = AUTH_KEY . SECURE_AUTH_KEY . LOGGED_IN_KEY . NONCE_KEY;
    
    // Derive a key using HKDF (key derivation function)
    $encryption_key = hash_hkdf(
        'sha256',                    // hash algorithm
        $key_material,               // input key material
        32,                          // output length (256 bits)
        'vertex-ai-encryption-key',  // context/purpose
        AUTH_SALT                    // salt
    );
    
    return $encryption_key;
}
Encrypting Sensitive Data
php
/**
 * Encrypt sensitive data before storage
 */
function encrypt_data($plaintext) {
    $key = get_encryption_key();
    
    // Generate random IV
    $iv_length = openssl_cipher_iv_length('aes-256-cbc');
    $iv = openssl_random_pseudo_bytes($iv_length);
    
    // Encrypt
    $encrypted = openssl_encrypt(
        $plaintext,
        'aes-256-cbc',
        $key,
        OPENSSL_RAW_DATA,
        $iv
    );
    
    // Combine IV and encrypted data
    $result = base64_encode($iv . $encrypted);
    
    return $result;
}

/**
 * Decrypt sensitive data
 */
function decrypt_data($encrypted_data) {
    $key = get_encryption_key();
    $data = base64_decode($encrypted_data);
    
    // Extract IV
    $iv_length = openssl_cipher_iv_length('aes-256-cbc');
    $iv = substr($data, 0, $iv_length);
    $encrypted = substr($data, $iv_length);
    
    // Decrypt
    $plaintext = openssl_decrypt(
        $encrypted,
        'aes-256-cbc',
        $key,
        OPENSSL_RAW_DATA,
        $iv
    );
    
    return $plaintext;
}
Storing API Credentials
php
// SECURE: Encrypt before storing
$api_key = sanitize_text_field($_POST['api_key']);
$encrypted_key = encrypt_data($api_key);
update_option('vertex_ai_api_key', $encrypted_key, false); // autoload = false

// SECURE: Decrypt when retrieving
$encrypted_key = get_option('vertex_ai_api_key');
$api_key = decrypt_data($encrypted_key);

// INSECURE: Never do this
update_option('api_key', $_POST['api_key']); // Plain text storage - BAD
What NOT to Do
php
// NEVER store encryption keys in:
// - Database
// - wp-content files
// - Version control
// - JavaScript
// - HTML comments
// - Cookie values

// VULNERABLE PATTERN - DO NOT USE
define('ENCRYPTION_KEY', 'hardcoded-key-123'); // BAD
update_option('encryption_key', wp_generate_password(32)); // BAD
Environment Variables (Alternative)
php
// If using environment variables (wp-config.php)
define('VERTEX_AI_API_KEY', getenv('VERTEX_AI_API_KEY'));

// Access
$api_key = defined('VERTEX_AI_API_KEY') ? VERTEX_AI_API_KEY : '';

// Ensure wp-config.php is not in version control

7. File Upload Security

Core Principle

Validate everything about uploaded files. File type, size, name, content, destination.

Secure File Upload Handler
php
function handle_file_upload() {
    // 1. Check nonce
    check_ajax_referer('file_upload_nonce', 'nonce');
    
    // 2. Check capability
    if (!current_user_can('upload_files')) {
        wp_send_json_error('Insufficient permissions');
    }
    
    // 3. Validate file was uploaded
    if (!isset($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) {
        wp_send_json_error('Upload failed');
    }
    
    // 4. Validate file size
    $max_size = 5 * 1024 * 1024; // 5MB
    if ($_FILES['file']['size'] > $max_size) {
        wp_send_json_error('File too large');
    }
    
    // 5. Validate file type
    $allowed_types = array('image/jpeg', 'image/png', 'application/pdf');
    $file_type = wp_check_filetype_and_ext(
        $_FILES['file']['tmp_name'],
        $_FILES['file']['name']
    );
    
    if (!in_array($file_type['type'], $allowed_types)) {
        wp_send_json_error('Invalid file type');
    }
    
    // 6. Sanitize filename
    $filename = sanitize_file_name($_FILES['file']['name']);
    
    // 7. Use WordPress upload handler (handles security)
    $upload = wp_handle_upload($_FILES['file'], array(
        'test_form' => false,
        'mimes' => array(
            'jpg|jpeg|jpe' => 'image/jpeg',
            'png' => 'image/png',
            'pdf' => 'application/pdf'
        )
    ));
    
    if (isset($upload['error'])) {
        wp_send_json_error($upload['error']);
    }
    
    // 8. Store file info securely
    $file_data = array(
        'url' => esc_url_raw($upload['url']),
        'path' => sanitize_text_field($upload['file']),
        'type' => sanitize_mime_type($upload['type'])
    );
    
    wp_send_json_success($file_data);
}
Image-Specific Validation
php
function validate_image_upload($file_path) {
    // Verify it's actually an image
    $image_info = getimagesize($file_path);
    
    if ($image_info === false) {
        return new WP_Error('invalid_image', 'Not a valid image');
    }
    
    // Check dimensions
    list($width, $height) = $image_info;
    
    if ($width > 4000 || $height > 4000) {
        return new WP_Error('image_too_large', 'Image dimensions too large');
    }
    
    // Verify MIME type matches extension
    $allowed_types = array(IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_GIF);
    
    if (!in_array($image_info[2], $allowed_types)) {
        return new WP_Error('invalid_type', 'Invalid image type');
    }
    
    return true;
}
CSV Upload (External Data)
php
function import_csv() {
    // Validate upload
    if (!isset($_FILES['csv']) || $_FILES['csv']['error'] !== UPLOAD_ERR_OK) {
        return new WP_Error('upload_error', 'CSV upload failed');
    }
    
    // Validate extension
    $file_ext = strtolower(pathinfo($_FILES['csv']['name'], PATHINFO_EXTENSION));
    if ($file_ext !== 'csv') {
        return new WP_Error('invalid_file', 'Must be a CSV file');
    }
    
    // Read and validate content
    $handle = fopen($_FILES['csv']['tmp_name'], 'r');
    
    if ($handle === false) {
        return new WP_Error('read_error', 'Cannot read CSV');
    }
    
    // Process rows
    $row_count = 0;
    while (($data = fgetcsv($handle)) !== false) {
        // Sanitize each cell
        $sanitized_row = array_map('sanitize_text_field', $data);
        
        // Validate data
        if (count($sanitized_row) < 3) {
            continue; // Skip invalid rows
        }
        
        // Process row
        // ...
        
        $row_count++;
        
        // Limit rows to prevent DoS
        if ($row_count > 10000) {
            break;
        }
    }
    
    fclose($handle);
    
    // Delete temp file
    unlink($_FILES['csv']['tmp_name']);
    
    return $row_count;
}

8. External API Security

Core Principle

Validate all API responses. Never trust external data, implement rate limiting, handle errors securely.

Secure API Request Pattern
php
function make_vertex_ai_request($endpoint, $data) {
    // 1. Get and decrypt API key
    $encrypted_key = get_option('vertex_ai_api_key');
    $api_key = decrypt_data($encrypted_key);
    
    if (empty($api_key)) {
        return new WP_Error('missing_key', 'API key not configured');
    }
    
    // 2. Build request
    $url = 'https://api.vertex-ai.google.com/' . sanitize_text_field($endpoint);
    
    $args = array(
        'method' => 'POST',
        'timeout' => 30,
        'headers' => array(
            'Authorization' => 'Bearer ' . $api_key,
            'Content-Type' => 'application/json'
        ),
        'body' => wp_json_encode($data),
        'sslverify' => true // ALWAYS verify SSL
    );
    
    // 3. Make request with error handling
    $response = wp_remote_post($url, $args);
    
    // 4. Check for HTTP errors
    if (is_wp_error($response)) {
        error_log('Vertex AI API Error: ' . $response->get_error_message());
        return new WP_Error('api_error', 'API request failed');
    }
    
    // 5. Check response code
    $response_code = wp_remote_retrieve_response_code($response);
    
    if ($response_code !== 200) {
        error_log('Vertex AI API returned code: ' . $response_code);
        return new WP_Error('api_error', 'API returned error: ' . $response_code);
    }
    
    // 6. Get and validate response body
    $body = wp_remote_retrieve_body($response);
    $parsed = json_decode($body, true);
    
    if (json_last_error() !== JSON_ERROR_NONE) {
        error_log('Vertex AI API returned invalid JSON');
        return new WP_Error('invalid_response', 'Invalid API response');
    }
    
    // 7. Validate response structure
    if (!isset($parsed['recommendations']) || !is_array($parsed['recommendations'])) {
        return new WP_Error('invalid_structure', 'Unexpected response structure');
    }
    
    // 8. Sanitize response data
    $recommendations = array();
    foreach ($parsed['recommendations'] as $item) {
        $recommendations[] = array(
            'product_id' => absint($item['product_id'] ?? 0),
            'score' => floatval($item['score'] ?? 0),
            'title' => sanitize_text_field($item['title'] ?? '')
        );
    }
    
    return $recommendations;
}
Rate Limiting
php
function check_api_rate_limit($user_id = null) {
    $user_id = $user_id ?? get_current_user_id();
    
    // Get transient for rate limiting
    $transient_key = 'api_calls_' . $user_id;
    $call_count = get_transient($transient_key);
    
    if ($call_count === false) {
        // First call in this period
        set_transient($transient_key, 1, HOUR_IN_SECONDS);
        return true;
    }
    
    // Check limit (100 calls per hour)
    if ($call_count >= 100) {
        return new WP_Error('rate_limit', 'Rate limit exceeded');
    }
    
    // Increment counter
    set_transient($transient_key, $call_count + 1, HOUR_IN_SECONDS);
    
    return true;
}

// Use before API call
$rate_check = check_api_rate_limit();
if (is_wp_error($rate_check)) {
    wp_send_json_error($rate_check->get_error_message());
}
Retry Logic with Exponential Backoff
php
function api_request_with_retry($endpoint, $data, $max_retries = 3) {
    $attempt = 0;
    
    while ($attempt < $max_retries) {
        $result = make_vertex_ai_request($endpoint, $data);
        
        // Success
        if (!is_wp_error($result)) {
            return $result;
        }
        
        // Don't retry client errors (4xx)
        $error_code = $result->get_error_code();
        if (in_array($error_code, array('invalid_key', 'missing_key'))) {
            return $result;
        }
        
        $attempt++;
        
        // Exponential backoff: 1s, 2s, 4s
        if ($attempt < $max_retries) {
            sleep(pow(2, $attempt - 1));
        }
    }
    
    return new WP_Error('max_retries', 'Maximum retry attempts exceeded');
}
Webhook Validation
php
function validate_webhook_signature() {
    // Get raw POST body
    $body = file_get_contents('php://input');
    
    // Get signature from header
    $signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
    
    if (empty($signature)) {
        wp_die('Missing signature', 403);
    }
    
    // Get webhook secret
    $secret = get_option('webhook_secret');
    
    // Calculate expected signature
    $expected = hash_hmac('sha256', $body, $secret);
    
    // Constant-time comparison to prevent timing attacks
    if (!hash_equals($expected, $signature)) {
        wp_die('Invalid signature', 403);
    }
    
    // Parse and process webhook
    $data = json_decode($body, true);
    
    // Validate structure
    if (!isset($data['event']) || !isset($data['timestamp'])) {
        wp_die('Invalid webhook data', 400);
    }
    
    // Check timestamp to prevent replay attacks (within 5 minutes)
    $timestamp = absint($data['timestamp']);
    if (abs(time() - $timestamp) > 300) {
        wp_die('Webhook expired', 400);
    }
    
    // Process webhook
    // ...
}

9. Common Vulnerabilities to Avoid

Direct File Access
php
// ALWAYS add this at the top of every PHP file
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
}
eval() and Dynamic Code Execution
php
// NEVER use eval()
eval($_POST['code']); // EXTREMELY DANGEROUS

// NEVER use create_function()
$func = create_function('$a', 'return ' . $_POST['expression'] . ';'); // DANGEROUS

// NEVER use variable functions with user input
$function = $_POST['function'];
$function(); // DANGEROUS
Unserialize User Input
php
// NEVER unserialize user input
$data = unserialize($_POST['data']); // DANGEROUS - Object injection vulnerability

// Use JSON instead
$data = json_decode($_POST['data'], true); // SAFE
Information Disclosure
php
// NEVER expose sensitive info in error messages
// BAD
if (!$user) {
    wp_die('User not found in database table wp_users');
}

// GOOD
if (!$user) {
    wp_die(esc_html__('Invalid user', 'textdomain'));
}

// NEVER output debug info to users
// BAD
echo 'SQL: ' . $wpdb->last_query;
echo 'Error: ' . $wpdb->last_error;

// GOOD (log instead)
error_log('SQL Error: ' . $wpdb->last_error);
Server-Side Request Forgery (SSRF)
php
// NEVER make requests to user-provided URLs without validation
// BAD
$url = $_POST['url'];
wp_remote_get($url); // DANGEROUS - can access internal resources

// GOOD - Validate URL
$url = esc_url_raw($_POST['url'], array('http', 'https'));

// Check it's not a private/local IP
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['host'])) {
    return new WP_Error('invalid_url', 'Invalid URL');
}

// Block private IPs
$ip = gethostbyname($parsed['host']);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
    return new WP_Error('private_ip', 'Private IP addresses not allowed');
}
Path Traversal
php
// NEVER use user input in file paths without validation
// BAD
$file = $_GET['file'];
include($file); // DANGEROUS - can include any file

// GOOD - Validate against whitelist
$allowed_files = array('template1.php', 'template2.php');
$file = sanitize_file_name($_GET['file']);

if (!in_array($file, $allowed_files)) {
    wp_die('Invalid file');
}

include(plugin_dir_path(__FILE__) . 'templates/' . $file);

10. Security Checklist

Use this checklist before releasing or deploying code:

Input/Output
  • All user input sanitized with appropriate functions
  • All output escaped for context (HTML, URL, JS, attributes)
  • No direct $_POST, $_GET, $_REQUEST access without sanitization
  • JSON responses use wp_send_json_* functions
Show full SKILL.md (237 more words)Show less
Authentication/Authorization
  • All forms have nonce fields
  • All state-changing operations verify nonces
  • AJAX handlers use check_ajax_referer()
  • All operations check current_user_can()
  • Admin pages check capabilities
Database
  • All queries use $wpdb->prepare() or equivalent
  • No string concatenation in SQL
  • LIKE queries use $wpdb->esc_like()
  • Array values properly sanitized before IN clauses
Files
  • All files start with if (!defined('ABSPATH')) exit;
  • File uploads validate type, size, and content
  • File paths don't use user input directly
  • Uploaded files stored in secure location
API/External Data
  • API credentials encrypted before storage
  • SSL verification enabled (sslverify => true)
  • API responses validated and sanitized
  • Rate limiting implemented
  • Timeout values set
  • Error messages don't leak sensitive info
Encryption
  • Sensitive data encrypted at rest
  • Encryption keys derived from WordPress salts (HKDF)
  • No hardcoded keys or secrets
  • API keys not in version control
General
  • No eval(), create_function(), or dynamic code execution
  • No unserialize() of user input
  • Error logging used instead of displaying errors
  • Debug mode disabled in production
  • Regular security updates and dependency checks

11. Additional Resources

WordPress Security Documentation
WooCommerce Specific
Tools

Conclusion

Security is not optional. Every line of code that handles user input, database queries, file operations, or external API calls must follow these patterns. When in doubt, be more restrictive rather than more permissive.

Remember: Security is a mindset, not a checklist.

© duracelltomi, GPL-2.0-or-later. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/wordpress-security of duracelltomi/gtm4wp.

Open the folder on GitHubat commit dfe20ee

Compare with similar skills

Wordpress Security next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Wordpress Security compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Wordpress Security this skillduracelltomi/gtm4wp174—~7.2kAutomated safety check: PassGPL-2.0-or-later
Keeljoseconti/declaracion-renta-espana190—~11kAutomated safety check: WarnGPL-3.0-or-later
Wordpresssickn33/agentic-awesome-skills47k2 repos~348Automated safety check: PassMIT
Php Wordpress Audit0xShe/PHP-Code-Audit-Skill4021 repos~666Automated safety check: PassNone
Wordpress Plugin Coresecondsky/claude-skills227—~4.6kAutomated safety check: PassMIT
Wp Security Reviewjorgerosal/wordpress-skills101—~6.4kAutomated safety check: PassMIT

Similar skills

  • Keel

    joseconti/declaracion-renta-espana

    A skill your agent uses for ANY new software project from idea to release — websites, WordPress/WooCommerce plugins, MCP servers, web apps, components, or libraries.

    190 GitHub stars~11k tokensUpdated 2 mo ago
    SecurityAuto-check: warnings
  • Wordpress

    sickn33/agentic-awesome-skills

    Complete WordPress development workflow covering theme development, plugin creation, WooCommerce integration, performance optimization, and security hardening.

    47k GitHub starsUsed in 2 repos~348 tokens
    SecurityAuto-check passed
  • Php Wordpress Audit

    0xShe/PHP-Code-Audit-Skill

    WordPress 框架特效安全审计工具。针对 WordPress 常见 nonce/capability/checkadminreferer、AJAX action、escape/sanitize、重定向、安全上传与远程请求等机制进行白盒静态审计,并映射到通用漏洞类型体系(AUTH/CSRF/XSS/SQL/CFG/SSRF 等)。

    402 GitHub starsUsed in 1 repo~666 tokens
    SecurityAuto-check passed
  • Wordpress Plugin Core

    secondsky/claude-skills

    WordPress plugin development with hooks, security, REST API, custom post types.

    227 GitHub stars~4.6k tokensUpdated 10 days ago
    SecurityAuto-check passed
  • Wp Security Review

    jorgerosal/wordpress-skills

    WordPress security code review and vulnerability detection. An agent skill from jorgerosal/wordpress-skills.

    101 GitHub stars~6.4k tokensUpdated 4 mo ago
    SecurityAuto-check passed
  • Audit PHP web application source for critical vulnerabilities using PHP's specific sink and footgun catalog — object injection via unserialize and phar:// POP chains, type-juggling and magic-hash…

    156 GitHub stars~2.8k tokensUpdated 1 mo ago
    SecurityAuto-check passed

More from duracelltomi/gtm4wp

  • Wporg Forum Triage

    duracelltomi/gtm4wp

    Triage GTM4WP support topics and reviews on the wordpress.org forum — read a topic (or a batch), work out whether it is already fixed in a released version, classify it, screen for security…

    174 GitHub stars~6.1k tokensUpdated yesterday
    Auto-check: warnings
  • Changelog

    duracelltomi/gtm4wp

    How to write GTM4WP CHANGELOG.md / readme.txt entries. An agent skill from duracelltomi/gtm4wp.

    174 GitHub stars~2.2k tokensUpdated yesterday
    Auto-check passed
  • GitHub Issue Triage

    duracelltomi/gtm4wp

    Triage and manage GTM4WP GitHub issues — read an issue (or a batch), classify it, check for duplicates/already-fixed, screen for security disclosures, and draft a polite reply plus proposed labels.

    174 GitHub stars~4.7k tokensUpdated yesterday
    Auto-check: warnings
  • Release

    duracelltomi/gtm4wp

    Cut a GTM4WP release — pre-flight verification, the version bumps, tag, ZIP, GitHub release with post-upload verification, branch mechanics, and the propagation sweep that updates RELEASE-STATE.md…

    174 GitHub stars~2.2k tokensUpdated yesterday
    Auto-check passed
  • Guide to create WooCommerce related WordPress plugins that extends WooCommerce functionality with a consistent and maintainable approach.

    174 GitHub stars~8.9k tokensUpdated yesterday
    Auto-check passed

Questions about Wordpress Security

What does Wordpress Security do?

Guide to maintain creating modern and secure code while developing WordPress plugins. Wordpress Security is an agent skill from duracelltomi/gtm4wp. Guide to maintain creating modern and secure code while developing WordPress plugins.

When should I use Wordpress Security?

Wordpress Security fits situations like: tasks that involve Web application vulnerabilities.

How do I install Wordpress Security in Claude Code?

Run `npx skills add duracelltomi/gtm4wp --skill wordpress-security -a claude-code`. Or copy the skill folder (.claude/skills/wordpress-security in duracelltomi/gtm4wp) into .claude/skills/wordpress-security in your project. Claude Code loads it when a task matches its description.

How do I install Wordpress Security in Codex?

Run `npx skills add duracelltomi/gtm4wp --skill wordpress-security -a codex`. Or copy the skill folder (.claude/skills/wordpress-security in duracelltomi/gtm4wp) into .agents/skills/wordpress-security in your project. Codex loads it when a task matches its description.

Can I use Wordpress Security in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add duracelltomi/gtm4wp --skill wordpress-security -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/wordpress-security, .gemini/skills/wordpress-security, .github/skills/wordpress-security and .opencode/skills/wordpress-security in your project.

What does Wordpress Security need to run?

Going by SKILL.md and its folder, Wordpress Security needs credentials named VERTEX_AI_API_KEY, AUTH_KEY, SECURE_AUTH_KEY and LOGGED_IN_KEY. Our summary lists: A credential in AUTH_KEY; A credential in SECURE_AUTH_KEY.

Does Wordpress Security access the network?

SKILL.md names 5 domains. In commands or code: api.vertex-ai.google.com; the agent is likely to contact it when it follows the instructions. As links in the text: developer.wordpress.org, github.com, woocommerce.com and wordpress.org. This is read from the text; nothing was executed.

Is Wordpress Security safe to install?

Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.

What licence does Wordpress Security use?

Wordpress Security is published under the GPL-2.0-or-later licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Wordpress Security use?

About 7.2k tokens (SKILL.md is roughly 29k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Wordpress Security?

Skills that share tags, products or a category with Wordpress Security: Keel (joseconti/declaracion-renta-espana, 190 stars), Wordpress (sickn33/agentic-awesome-skills, 47k stars), Php Wordpress Audit (0xShe/PHP-Code-Audit-Skill, 402 stars) and Wordpress Plugin Core (secondsky/claude-skills, 227 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Wordpress Security?

duracelltomi (a GitHub user) maintains it in duracelltomi/gtm4wp, which has 174 GitHub stars. The repository holds 6 skills in this directory. The repository was last updated on October 7, 2026.

Source: duracelltomi/gtm4wp on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.