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.
Guide to maintain creating modern and secure code while developing WordPress plugins.
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ 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-srcUse ~/.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/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .claude/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-securityType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/wordpress-security .agents/skills/wordpress-security && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .agents/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/wordpress-security .cursor/skills/wordpress-security && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .cursor/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/duracelltomi/gtm4wp.git --path .claude/skills/wordpress-security--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/wordpress-security .gemini/skills/wordpress-security && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .gemini/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install duracelltomi/gtm4wp wordpress-securityInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/wordpress-security .github/skills/wordpress-security && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .github/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add duracelltomi/gtm4wp --skill wordpress-security -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install duracelltomi/gtm4wp wordpress-security --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/duracelltomi/gtm4wp.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/wordpress-security .opencode/skills/wordpress-security && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "wordpress-security" agent skill from https://github.com/duracelltomi/gtm4wp/tree/master/.claude/skills/wordpress-security into .opencode/skills/wordpress-security/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "wordpress-security", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
wordpress-securityGuide 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.
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.
11 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit dfe20ee. It shows what the files ask for, not the result of running them.
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.
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.
Hosts in commands or code, which the agent is likely to contact:
api.vertex-ai.google.comAlso links to:
developer.wordpress.orggithub.comwoocommerce.comwordpress.orgFrom URLs in SKILL.md, links to its own repository left out.
Names these keys or tokens, usually read from environment variables:
VERTEX_AI_API_KEYAUTH_KEYSECURE_AUTH_KEYLOGGED_IN_KEYNONCE_KEYENCRYPTION_KEYFrom names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
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.
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.
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.
The full file from duracelltomi/gtm4wp at commit dfe20ee, republished under its GPL-2.0-or-later licence (© duracelltomi). 586 words, ~7,162 tokens.
.claude/skills/wordpress-security/SKILL.md (or your agent's skills folder).Comprehensive security guidelines for WordPress plugin development, with emphasis on WooCommerce extensions and external API integrations.
NEVER trust user input. All data from users, URLs, forms, AJAX requests, or external APIs must be sanitized before processing or storage.
// 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']);// 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']);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;
}Escape late, escape often. Always escape data when outputting to HTML, JavaScript, URLs, or attributes.
// 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 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">';
}
);// 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);Verify intent. All state-changing operations must verify a nonce to prevent Cross-Site Request Forgery.
// 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'));
}// 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'));
}// 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'));
}// 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)
});ALWAYS use prepared statements. Never concatenate user input into SQL queries.
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')
);// %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
);// 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
));$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 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%'"
);Check permissions early and often. Never assume a user has the right to perform an action.
// 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;
}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
}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
// ...
}// 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');
}Never store sensitive data in plain text. Use WordPress salts for key derivation, encrypt at rest, protect in transit.
/**
* 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;
}/**
* 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;
}// 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// 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// 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 controlValidate everything about uploaded files. File type, size, name, content, destination.
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);
}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;
}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;
}Validate all API responses. Never trust external data, implement rate limiting, handle errors securely.
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;
}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());
}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');
}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
// ...
}// ALWAYS add this at the top of every PHP file
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}// 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// NEVER unserialize user input
$data = unserialize($_POST['data']); // DANGEROUS - Object injection vulnerability
// Use JSON instead
$data = json_decode($_POST['data'], true); // SAFE// 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);// 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');
}// 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);Use this checklist before releasing or deploying code:
$_POST, $_GET, $_REQUEST access without sanitizationwp_send_json_* functionscheck_ajax_referer()current_user_can()$wpdb->prepare() or equivalent$wpdb->esc_like()if (!defined('ABSPATH')) exit;sslverify => true)eval(), create_function(), or dynamic code executionunserialize() of user inputSecurity 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
Just SKILL.md in .claude/skills/wordpress-security of duracelltomi/gtm4wp.
Open the folder on GitHubat commit dfe20ee
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.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Wordpress Security this skillduracelltomi/gtm4wp | 174 | — | ~7.2k | Automated safety check: Pass | GPL-2.0-or-later | |
| Keeljoseconti/declaracion-renta-espana | 190 | — | ~11k | Automated safety check: Warn | GPL-3.0-or-later | |
| Wordpresssickn33/agentic-awesome-skills | 47k | 2 repos | ~348 | Automated safety check: Pass | MIT | |
| Php Wordpress Audit0xShe/PHP-Code-Audit-Skill | 402 | 1 repos | ~666 | Automated safety check: Pass | None | |
| Wordpress Plugin Coresecondsky/claude-skills | 227 | — | ~4.6k | Automated safety check: Pass | MIT | |
| Wp Security Reviewjorgerosal/wordpress-skills | 101 | — | ~6.4k | Automated safety check: Pass | MIT |
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.
sickn33/agentic-awesome-skills
Complete WordPress development workflow covering theme development, plugin creation, WooCommerce integration, performance optimization, and security hardening.
0xShe/PHP-Code-Audit-Skill
WordPress 框架特效安全审计工具。针对 WordPress 常见 nonce/capability/checkadminreferer、AJAX action、escape/sanitize、重定向、安全上传与远程请求等机制进行白盒静态审计,并映射到通用漏洞类型体系(AUTH/CSRF/XSS/SQL/CFG/SSRF 等)。
secondsky/claude-skills
WordPress plugin development with hooks, security, REST API, custom post types.
jorgerosal/wordpress-skills
WordPress security code review and vulnerability detection. An agent skill from jorgerosal/wordpress-skills.
trilwu/secskills
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…
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…
duracelltomi/gtm4wp
How to write GTM4WP CHANGELOG.md / readme.txt entries. An agent skill from duracelltomi/gtm4wp.
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.
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…
duracelltomi/gtm4wp
Guide to create WooCommerce related WordPress plugins that extends WooCommerce functionality with a consistent and maintainable approach.
Works with
Categories
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.
Wordpress Security fits situations like: tasks that involve Web application vulnerabilities.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.