--- 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 ''; // URL echo 'Link'; // JavaScript echo ''; // Textarea content echo ''; // 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 here', 'textdomain'), esc_url($url)) ); ``` ### Admin UI Escaping ```php // Admin notices echo '

' . esc_html__('Settings saved successfully.', 'textdomain') . '

'; // Settings field add_settings_field( 'api_key', esc_html__('API Key', 'textdomain'), function($args) { $value = get_option('my_api_key'); echo ''; } ); ``` ### 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
// 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 'Delete'; // 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. ### Key Derivation (Recommended Pattern) ```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 ### 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 - [WordPress Plugin Security Guidelines](https://developer.wordpress.org/plugins/security/) - [Data Validation](https://developer.wordpress.org/plugins/security/data-validation/) - [Nonces](https://developer.wordpress.org/plugins/security/nonces/) ### WooCommerce Specific - [WooCommerce Security Best Practices](https://woocommerce.com/document/woocommerce-security/) - [WooCommerce Coding Standards](https://github.com/woocommerce/woocommerce/wiki/JavaScript-Coding-Standards) ### Tools - [Plugin Check Plugin](https://wordpress.org/plugins/plugin-check/) - Automated security checks - [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) - Code standards checking - [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards) --- ## 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.**