[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6F45ABCTG9PJARGYNZ75FHG":3,"skill-same:01M6F45ABCTG9PJARGYNZ75FHG":82},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":28,"publishedAt":49,"linkOnly":50,"markdownContent":51,"markdownBody":52,"frontMatter":53,"refName":59,"commitSha":60,"skillDirectory":61,"forksCount":62,"repositoryHtmlUrl":63,"repositoryHomepageUrl":64,"ownerHtmlUrl":65,"ownerBlogUrl":6,"ownerDescription":66,"updatedAt":67,"install":68,"resources":81},"01M6F45ABCTG9PJARGYNZ75FHG","wp-plugin-security",null,"Security guidelines for WordPress plugin development: sanitization, validation, escaping, nonces, capabilities, SQL injection prevention, XSS protection, and CSRF mitigation. Based on official WordPress Developer Resources.","ComeOnOliver\u002Fskillshub","ComeOnOliver","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F43215937?v=4","01M6E5JNFECJAAVFE0W7SMWWD2","Haoran Shu","01M6E5JNNDQS7AJZDYFQ8TPZCK",67,"GPL-2.0","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Ffernandotellado\u002Fai-skills\u002Fwp-plugin-security\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":26,"taxonomyTypeDesc":27},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48],"wp plugin security","plugin","security","plugins","themes","resources","wordpress plugin development","csrf","prevention","xss","injection","developer","capabilities","validation","code","development","guidelines","sql","official","wordpress","2026-11-10 10:12:30",false,"---\nname: wp-plugin-security\ndescription: \"Security guidelines for WordPress plugin development: sanitization, validation, escaping, nonces, capabilities, SQL injection prevention, XSS protection, and CSRF mitigation. Based on official WordPress Developer Resources.\"\ncompatibility: \"WordPress 6.0+ \u002F PHP 7.4+. Applies to plugins, themes, and custom code.\"\nlicense: GPL-2.0-or-later\nmetadata:\n  author: fernando-tellado\n  version: \"1.0\"\n---\n\n# WordPress plugin security\n\n## When to use\n\nUse this skill when:\n\n- Developing new WordPress plugins or themes\n- Reviewing existing code for security vulnerabilities\n- Handling user input (forms, AJAX, REST API)\n- Outputting dynamic content to the browser\n- Interacting with the database\n- Creating admin pages or settings\n- Implementing AJAX or REST endpoints\n- Processing file uploads\n\n## Core security principles\n\n### The security mantra\n\n```\nSanitize early\nEscape late\nAlways validate\nNever trust user input\n```\n\n### Key concepts\n\n1. **Sanitization**: Clean\u002Ffilter input data as soon as it is received\n2. **Validation**: Verify data matches expected format\u002Fvalues (prefer over sanitization)\n3. **Escaping**: Secure output data before rendering to prevent XSS\n4. **Nonces**: Protect against CSRF attacks on forms and URLs\n5. **Capabilities**: Verify user has permission to perform actions\n\n## Sanitization\n\nSanitize input data immediately upon receipt. Use the most specific function available.\n\n### Sanitization functions\n\n| Function | Use case |\n|----------|----------|\n| `sanitize_text_field()` | Single-line text input |\n| `sanitize_textarea_field()` | Multi-line text input |\n| `sanitize_email()` | Email addresses |\n| `sanitize_file_name()` | File names |\n| `sanitize_hex_color()` | Color values with hash |\n| `sanitize_hex_color_no_hash()` | Color values without hash |\n| `sanitize_html_class()` | HTML class names |\n| `sanitize_key()` | Keys (lowercase alphanumeric, dashes, underscores) |\n| `sanitize_meta()` | Meta values |\n| `sanitize_mime_type()` | MIME types |\n| `sanitize_option()` | Option values |\n| `sanitize_sql_orderby()` | SQL ORDER BY clauses |\n| `sanitize_title()` | Titles\u002Fslugs |\n| `sanitize_title_with_dashes()` | URL-friendly titles |\n| `sanitize_user()` | Usernames |\n| `sanitize_url()` | URLs for storage |\n| `wp_kses()` | HTML with allowed tags |\n| `wp_kses_post()` | HTML allowed in posts |\n\n### Sanitization example\n\n```php\n\u002F\u002F Sanitize a text field from POST\n$title = sanitize_text_field( $_POST['title'] ?? '' );\n\n\u002F\u002F Sanitize email\n$email = sanitize_email( $_POST['email'] ?? '' );\n\n\u002F\u002F Sanitize URL for database storage\n$url = sanitize_url( $_POST['website'] ?? '' );\n\n\u002F\u002F Sanitize textarea\n$description = sanitize_textarea_field( $_POST['description'] ?? '' );\n```\n\n### Important notes on sanitization\n\n- **Never use escape functions for sanitization** - they serve different purposes\n- When using `filter_var()`, always specify a sanitizing filter (not `FILTER_DEFAULT`)\n- Process only the specific keys you need, not the entire `$_POST`\u002F`$_GET` array\n\n```php\n\u002F\u002F CORRECT: Specify sanitizing filter\n$post_id = filter_input( INPUT_GET, 'post_id', FILTER_SANITIZE_NUMBER_INT );\n\n\u002F\u002F WRONG: No filter or FILTER_DEFAULT does not sanitize\n$post_id = filter_input( INPUT_GET, 'post_id' ); \u002F\u002F Insecure!\n```\n\n## Validation\n\nValidation verifies data matches expected patterns. **Prefer validation over sanitization when possible.**\n\n### Validation philosophies\n\n#### Safelist (recommended)\n\nAccept only known, trusted values:\n\n```php\n$allowed_values = array( 'draft', 'pending', 'publish' );\n\n\u002F\u002F Use strict comparison (third parameter = true)\nif ( in_array( $status, $allowed_values, true ) ) {\n    \u002F\u002F Valid\n} else {\n    wp_die( 'Invalid status' );\n}\n```\n\n#### Format detection\n\nTest data format and reject if invalid:\n\n```php\n\u002F\u002F Check alphanumeric only\nif ( ! ctype_alnum( $data ) ) {\n    wp_die( 'Invalid format' );\n}\n\n\u002F\u002F Check against regex\nif ( ! preg_match( '\u002F^\\d{5}(-\\d{4})?$\u002F', $zip_code ) ) {\n    wp_die( 'Invalid ZIP code format' );\n}\n```\n\n#### Type checking\n\nAlways use strict comparison (`===`) to prevent type juggling attacks:\n\n```php\n\u002F\u002F CORRECT: Strict comparison\nif ( 1 === $user_input ) {\n    \u002F\u002F Exactly integer 1\n}\n\n\u002F\u002F WRONG: Loose comparison - \"1 malicious\" == 1 evaluates to true\nif ( 1 == $user_input ) {\n    \u002F\u002F Vulnerable!\n}\n```\n\n### Validation functions\n\n| Function | Purpose |\n|----------|---------|\n| `is_email()` | Validate email format |\n| `term_exists()` | Check if taxonomy term exists |\n| `username_exists()` | Check if username exists |\n| `validate_file()` | Validate file path (not existence) |\n| `is_array()` | Check if value is array |\n| `absint()` | Return absolute integer |\n| `in_array( $val, $arr, true )` | Check value in array (strict) |\n\n### Validation example\n\n```php\nfunction ayudawp_is_valid_us_zip( string $zip ): bool {\n    if ( empty( $zip ) ) {\n        return false;\n    }\n\n    if ( strlen( trim( $zip ) ) > 10 ) {\n        return false;\n    }\n\n    if ( ! preg_match( '\u002F^\\d{5}(-?\\d{4})?$\u002F', $zip ) ) {\n        return false;\n    }\n\n    return true;\n}\n\n\u002F\u002F Usage\nif ( isset( $_POST['zip'] ) && ayudawp_is_valid_us_zip( $_POST['zip'] ) ) {\n    $zip = sanitize_text_field( $_POST['zip'] );\n    \u002F\u002F Process valid ZIP\n}\n```\n\n## Escaping\n\nEscape output data **as late as possible**, immediately when echoing.\n\n### Escaping functions\n\n| Function | Use case |\n|----------|----------|\n| `esc_html()` | Text inside HTML elements |\n| `esc_attr()` | Values inside HTML attributes |\n| `esc_url()` | URLs in href, src attributes |\n| `esc_url_raw()` | URLs for database storage (NOT escaping) |\n| `esc_js()` | Inline JavaScript values |\n| `esc_textarea()` | Content inside textarea |\n| `esc_xml()` | XML content |\n| `wp_kses()` | HTML with custom allowed tags |\n| `wp_kses_post()` | HTML allowed in post content |\n| `wp_kses_data()` | HTML allowed in comments |\n\n### Escaping examples\n\n```php\n\u002F\u002F Text inside HTML element\n\u003Ch4>\u003C?php echo esc_html( $title ); ?>\u003C\u002Fh4>\n\n\u002F\u002F URL in attribute\n\u003Ca href=\"\u003C?php echo esc_url( $link ); ?>\">Link\u003C\u002Fa>\n\n\u002F\u002F Value in attribute\n\u003Cinput type=\"text\" value=\"\u003C?php echo esc_attr( $value ); ?>\">\n\n\u002F\u002F Image source\n\u003Cimg src=\"\u003C?php echo esc_url( $image_url ); ?>\" alt=\"\u003C?php echo esc_attr( $alt ); ?>\">\n\n\u002F\u002F Inline JavaScript\n\u003Cdiv onclick=\"doSomething('\u003C?php echo esc_js( $param ); ?>')\">\n\n\u002F\u002F Textarea content\n\u003Ctextarea>\u003C?php echo esc_textarea( $content ); ?>\u003C\u002Ftextarea>\n\n\u002F\u002F HTML content (preserves allowed HTML)\n\u003Cdiv>\u003C?php echo wp_kses_post( $html_content ); ?>\u003C\u002Fdiv>\n```\n\n### Escape late pattern\n\nAlways escape at the point of output:\n\n```php\n\u002F\u002F WRONG: Escaping early\n$url = esc_url( $url );\n$text = esc_html( $text );\necho '\u003Ca href=\"' . $url . '\">' . $text . '\u003C\u002Fa>';\n\n\u002F\u002F CORRECT: Escaping late\necho '\u003Ca href=\"' . esc_url( $url ) . '\">' . esc_html( $text ) . '\u003C\u002Fa>';\n```\n\n### Escaping with localization\n\nUse combined escape + localization functions:\n\n```php\n\u002F\u002F Escape + translate\necho esc_html__( 'Hello World', 'text-domain' );\nesc_html_e( 'Hello World', 'text-domain' );\n\n\u002F\u002F With context\necho esc_html_x( 'Post', 'noun', 'text-domain' );\n\n\u002F\u002F For attributes\necho esc_attr__( 'Submit', 'text-domain' );\nesc_attr_e( 'Submit', 'text-domain' );\n```\n\nAvailable combined functions:\n- `esc_html__()`, `esc_html_e()`, `esc_html_x()`\n- `esc_attr__()`, `esc_attr_e()`, `esc_attr_x()`\n\n### Important escaping notes\n\n- **Never use `__()` or `_e()` without escaping** - they do not escape output\n- **`esc_url_raw()` is NOT an escaping function** - it's for sanitizing URLs for storage\n- Use `wp_kses_post()` or `wp_kses()` for HTML output, NOT `esc_html()` which strips HTML\n- When escaping HTML attributes, escape the entire value, not parts\n\n```php\n\u002F\u002F CORRECT: Escape the whole attribute value\necho '\u003Cdiv id=\"' . esc_attr( $prefix . '-box-' . $id ) . '\">';\n\n\u002F\u002F WRONG: Escaping parts separately\necho '\u003Cdiv id=\"' . esc_attr( $prefix ) . '-box-' . esc_attr( $id ) . '\">';\n```\n\n### Custom HTML escaping with wp_kses\n\n```php\n$allowed_html = array(\n    'a'      => array(\n        'href'  => array(),\n        'title' => array(),\n    ),\n    'br'     => array(),\n    'em'     => array(),\n    'strong' => array(),\n);\n\necho wp_kses( $user_html, $allowed_html );\n```\n\n## Nonces\n\nNonces protect against CSRF (Cross-Site Request Forgery) attacks.\n\n### Creating nonces\n\n```php\n\u002F\u002F In a URL\n$url = wp_nonce_url( $base_url, 'delete-post_' . $post_id );\n\n\u002F\u002F In a form (echoes hidden fields)\nwp_nonce_field( 'save-settings_' . $user_id, 'ayudawp_nonce' );\n\n\u002F\u002F Get nonce value only\n$nonce = wp_create_nonce( 'my-action_' . $post_id );\n```\n\n### Verifying nonces\n\n```php\n\u002F\u002F In admin screens (also checks referrer)\ncheck_admin_referer( 'delete-post_' . $post_id, 'ayudawp_nonce' );\n\n\u002F\u002F In AJAX requests\ncheck_ajax_referer( 'my-ajax-action', 'security' );\n\n\u002F\u002F Manual verification\nif ( ! wp_verify_nonce( \n    sanitize_text_field( wp_unslash( $_POST['ayudawp_nonce'] ?? '' ) ), \n    'my-action_' . $post_id \n) ) {\n    wp_die( 'Security check failed' );\n}\n```\n\n### Nonce best practices\n\n- Make action strings specific: `'delete-post_' . $post_id` not just `'delete'`\n- Always sanitize nonce before verification:\n\n```php\n\u002F\u002F CORRECT: Sanitize nonce input\nif ( ! isset( $_POST['_wpnonce'] ) || \n     ! wp_verify_nonce( \n         sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), \n         'my_action' \n     ) \n) {\n    wp_die( 'Security check failed' );\n}\n```\n\n- Nonces have limited lifetime (default 24 hours, configurable)\n- **Nonces alone are not sufficient** - always combine with capability checks\n- Nonces are user-specific and session-specific\n\n### Modifying nonce lifetime\n\n```php\nadd_filter( 'nonce_life', function() {\n    return 4 * HOUR_IN_SECONDS;\n} );\n```\n\n## User capabilities\n\nAlways verify user has permission before performing actions.\n\n### Checking capabilities\n\n```php\n\u002F\u002F Check current user capability\nif ( ! current_user_can( 'edit_posts' ) ) {\n    wp_die( 'You do not have permission to do this.' );\n}\n\n\u002F\u002F Check capability for specific post\nif ( ! current_user_can( 'edit_post', $post_id ) ) {\n    wp_die( 'You cannot edit this post.' );\n}\n\n\u002F\u002F Check if user is admin\nif ( ! current_user_can( 'manage_options' ) ) {\n    wp_die( 'Administrator access required.' );\n}\n```\n\n### Common capabilities\n\n| Capability | Role level |\n|------------|------------|\n| `read` | Subscriber+ |\n| `edit_posts` | Contributor+ |\n| `publish_posts` | Author+ |\n| `edit_others_posts` | Editor+ |\n| `manage_options` | Administrator |\n| `edit_themes` | Administrator |\n| `activate_plugins` | Administrator |\n\n### Complete security check example\n\n```php\nfunction ayudawp_delete_item() {\n    \u002F\u002F 1. Check nonce\n    if ( ! isset( $_POST['_wpnonce'] ) ||\n         ! wp_verify_nonce( \n             sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), \n             'delete_item_' . absint( $_POST['item_id'] ?? 0 )\n         )\n    ) {\n        wp_die( 'Security check failed' );\n    }\n\n    \u002F\u002F 2. Check capability\n    if ( ! current_user_can( 'delete_posts' ) ) {\n        wp_die( 'You do not have permission to delete items.' );\n    }\n\n    \u002F\u002F 3. Validate and sanitize input\n    $item_id = absint( $_POST['item_id'] ?? 0 );\n    if ( ! $item_id ) {\n        wp_die( 'Invalid item ID' );\n    }\n\n    \u002F\u002F 4. Perform action\n    \u002F\u002F ... delete logic here\n}\n```\n\n## SQL injection prevention\n\n### Use $wpdb->prepare()\n\nAlways use prepared statements for database queries:\n\n```php\nglobal $wpdb;\n\n\u002F\u002F Single value\n$result = $wpdb->get_var( \n    $wpdb->prepare(\n        \"SELECT post_title FROM {$wpdb->posts} WHERE ID = %d\",\n        $post_id\n    )\n);\n\n\u002F\u002F Multiple values\n$results = $wpdb->get_results(\n    $wpdb->prepare(\n        \"SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_author = %d\",\n        $status,\n        $author_id\n    )\n);\n```\n\n### Placeholders\n\n| Placeholder | Type |\n|-------------|------|\n| `%d` | Integer |\n| `%f` | Float |\n| `%s` | String |\n| `%i` | Identifier (table\u002Fcolumn name, WP 6.2+) |\n\n### Arrays in queries\n\n```php\n\u002F\u002F Build placeholders for array\n$ids = array( 1, 2, 3, 4, 5 );\n$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );\n\n$results = $wpdb->get_results(\n    $wpdb->prepare(\n        \"SELECT * FROM {$wpdb->posts} WHERE ID IN ( $placeholders )\",\n        $ids\n    )\n);\n```\n\n### Use WordPress functions when possible\n\nPrefer WordPress API functions over direct SQL:\n\n```php\n\u002F\u002F PREFERRED: Use WordPress functions\nupdate_post_meta( $post_id, 'my_key', $value );\nget_option( 'my_option' );\nWP_Query for post queries\n\n\u002F\u002F AVOID: Direct SQL unless necessary\n$wpdb->query( \"INSERT INTO...\" );\n```\n\n## Common vulnerabilities\n\n### XSS (Cross-Site Scripting)\n\n**Prevention**: Escape all output\n\n```php\n\u002F\u002F Vulnerable\necho $user_input;\n\n\u002F\u002F Secure\necho esc_html( $user_input );\n```\n\n### CSRF (Cross-Site Request Forgery)\n\n**Prevention**: Use nonces + capability checks\n\n```php\n\u002F\u002F In form\nwp_nonce_field( 'my_action', 'my_nonce' );\n\n\u002F\u002F On submission\ncheck_admin_referer( 'my_action', 'my_nonce' );\nif ( ! current_user_can( 'manage_options' ) ) {\n    wp_die( 'Unauthorized' );\n}\n```\n\n### SQL Injection\n\n**Prevention**: Use prepared statements\n\n```php\n\u002F\u002F Vulnerable\n$wpdb->query( \"DELETE FROM table WHERE id = \" . $_GET['id'] );\n\n\u002F\u002F Secure\n$wpdb->query( \n    $wpdb->prepare( \"DELETE FROM table WHERE id = %d\", absint( $_GET['id'] ) )\n);\n```\n\n## File handling security\n\n### Use WordPress upload functions\n\n```php\n\u002F\u002F CORRECT: Use wp_handle_upload\n$uploaded = wp_handle_upload( $_FILES['my_file'], array( \n    'test_form' => false \n) );\n\n\u002F\u002F WRONG: Direct move_uploaded_file\nmove_uploaded_file( $_FILES['my_file']['tmp_name'], $destination );\n```\n\n### Never allow unfiltered uploads\n\n```php\n\u002F\u002F NEVER DO THIS\ndefine( 'ALLOW_UNFILTERED_UPLOADS', true );\n\n\u002F\u002F Instead, use upload_mimes filter for specific file types\nadd_filter( 'upload_mimes', function( $mimes ) {\n    $mimes['svg'] = 'image\u002Fsvg+xml';\n    return $mimes;\n} );\n```\n\n### Validate file types\n\n```php\n$allowed_types = array( 'image\u002Fjpeg', 'image\u002Fpng', 'image\u002Fgif' );\n$file_type = wp_check_filetype( $filename );\n\nif ( ! in_array( $file_type['type'], $allowed_types, true ) ) {\n    wp_die( 'Invalid file type' );\n}\n```\n\n## Direct file access prevention\n\nAdd to all PHP files that could execute code:\n\n```php\n\u003C?php\n\u002F\u002F Prevent direct file access\nif ( ! defined( 'ABSPATH' ) ) {\n    exit;\n}\n```\n\n## AJAX security\n\n### Register AJAX handlers\n\n```php\n\u002F\u002F For logged-in users\nadd_action( 'wp_ajax_my_action', 'ayudawp_ajax_handler' );\n\n\u002F\u002F For non-logged-in users (if needed)\nadd_action( 'wp_ajax_nopriv_my_action', 'ayudawp_ajax_handler' );\n\nfunction ayudawp_ajax_handler() {\n    \u002F\u002F 1. Verify nonce\n    check_ajax_referer( 'my_ajax_nonce', 'security' );\n\n    \u002F\u002F 2. Check capabilities\n    if ( ! current_user_can( 'edit_posts' ) ) {\n        wp_send_json_error( 'Unauthorized', 403 );\n    }\n\n    \u002F\u002F 3. Sanitize input\n    $data = sanitize_text_field( $_POST['data'] ?? '' );\n\n    \u002F\u002F 4. Process and respond\n    wp_send_json_success( array( 'result' => $data ) );\n}\n```\n\n### JavaScript side\n\n```php\n\u002F\u002F Localize script with nonce\nwp_localize_script( 'my-script', 'myAjax', array(\n    'ajaxurl' => admin_url( 'admin-ajax.php' ),\n    'nonce'   => wp_create_nonce( 'my_ajax_nonce' ),\n) );\n```\n\n```javascript\n\u002F\u002F AJAX call\njQuery.post( myAjax.ajaxurl, {\n    action: 'my_action',\n    security: myAjax.nonce,\n    data: 'my data'\n}, function( response ) {\n    \u002F\u002F Handle response\n});\n```\n\n## REST API security\n\n```php\nregister_rest_route( 'myplugin\u002Fv1', '\u002Fitems', array(\n    'methods'             => 'POST',\n    'callback'            => 'ayudawp_create_item',\n    'permission_callback' => function() {\n        return current_user_can( 'edit_posts' );\n    },\n    'args'                => array(\n        'title' => array(\n            'required'          => true,\n            'sanitize_callback' => 'sanitize_text_field',\n            'validate_callback' => function( $value ) {\n                return ! empty( $value );\n            },\n        ),\n    ),\n) );\n```\n\n## Code review checklist\n\n### Input handling\n\n- [ ] All `$_POST`, `$_GET`, `$_REQUEST` values are sanitized\n- [ ] All `$_FILES` uploads use `wp_handle_upload()`\n- [ ] Database queries use `$wpdb->prepare()`\n- [ ] Type casting used where appropriate (`absint()`, `(int)`, etc.)\n\n### Output handling\n\n- [ ] All dynamic output is escaped\n- [ ] Correct escape function used for context (html\u002Fattr\u002Furl\u002Fjs)\n- [ ] Escaping happens at output time (late escaping)\n- [ ] Translation functions are escaped (`esc_html__()` not `__()`)\n\n### Authentication & authorization\n\n- [ ] Nonces used on all forms and state-changing URLs\n- [ ] Nonces verified before processing actions\n- [ ] Capability checks performed before actions\n- [ ] Both nonce AND capability checked (not just one)\n\n### General\n\n- [ ] No `ALLOW_UNFILTERED_UPLOADS`\n- [ ] Direct file access prevented with `ABSPATH` check\n- [ ] No `error_reporting()` in production code\n- [ ] No timezone changes with `date_default_timezone_set()`\n- [ ] Uses WordPress HTTP API, not raw cURL\n- [ ] Uses `wp_enqueue_*` for scripts\u002Fstyles\n\n## WPCS security sniffs\n\nWordPress Coding Standards includes these security sniffs:\n\n- `EscapeOutputSniff` - Verifies output is escaped\n- `NonceVerificationSniff` - Verifies nonce checks\n- `ValidatedSanitizedInputSniff` - Verifies input sanitization\n- `SafeRedirectSniff` - Verifies safe redirects\n- `PluginMenuSlugSniff` - Verifies menu slug safety\n\nRun PHPCS with WordPress standards:\n\n```bash\nphpcs --standard=WordPress path\u002Fto\u002Fplugin\n```\n\n## References\n\n- [WordPress Security API](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002F)\n- [Escaping Data](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fescaping\u002F)\n- [Sanitizing Data](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fsanitizing\u002F)\n- [Data Validation](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fdata-validation\u002F)\n- [Nonces](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fnonces\u002F)\n- [User Roles and Capabilities](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fuser-roles-and-capabilities\u002F)\n- [Common Vulnerabilities](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fcommon-vulnerabilities\u002F)\n- [Plugin Review Team Common Issues](https:\u002F\u002Fdeveloper.wordpress.org\u002Fplugins\u002Fwordpress-org\u002Fcommon-issues\u002F)\n- [WordPress Coding Standards](https:\u002F\u002Fgithub.com\u002FWordPress\u002FWordPress-Coding-Standards)\n\n","# WordPress plugin security\n\n## When to use\n\nUse this skill when:\n\n- Developing new WordPress plugins or themes\n- Reviewing existing code for security vulnerabilities\n- Handling user input (forms, AJAX, REST API)\n- Outputting dynamic content to the browser\n- Interacting with the database\n- Creating admin pages or settings\n- Implementing AJAX or REST endpoints\n- Processing file uploads\n\n## Core security principles\n\n### The security mantra\n\n```\nSanitize early\nEscape late\nAlways validate\nNever trust user input\n```\n\n### Key concepts\n\n1. **Sanitization**: Clean\u002Ffilter input data as soon as it is received\n2. **Validation**: Verify data matches expected format\u002Fvalues (prefer over sanitization)\n3. **Escaping**: Secure output data before rendering to prevent XSS\n4. **Nonces**: Protect against CSRF attacks on forms and URLs\n5. **Capabilities**: Verify user has permission to perform actions\n\n## Sanitization\n\nSanitize input data immediately upon receipt. Use the most specific function available.\n\n### Sanitization functions\n\n| Function | Use case |\n|----------|----------|\n| `sanitize_text_field()` | Single-line text input |\n| `sanitize_textarea_field()` | Multi-line text input |\n| `sanitize_email()` | Email addresses |\n| `sanitize_file_name()` | File names |\n| `sanitize_hex_color()` | Color values with hash |\n| `sanitize_hex_color_no_hash()` | Color values without hash |\n| `sanitize_html_class()` | HTML class names |\n| `sanitize_key()` | Keys (lowercase alphanumeric, dashes, underscores) |\n| `sanitize_meta()` | Meta values |\n| `sanitize_mime_type()` | MIME types |\n| `sanitize_option()` | Option values |\n| `sanitize_sql_orderby()` | SQL ORDER BY clauses |\n| `sanitize_title()` | Titles\u002Fslugs |\n| `sanitize_title_with_dashes()` | URL-friendly titles |\n| `sanitize_user()` | Usernames |\n| `sanitize_url()` | URLs for storage |\n| `wp_kses()` | HTML with allowed tags |\n| `wp_kses_post()` | HTML allowed in posts |\n\n### Sanitization example\n\n```php\n\u002F\u002F Sanitize a text field from POST\n$title = sanitize_text_field( $_POST['title'] ?? '' );\n\n\u002F\u002F Sanitize email\n$email = sanitize_email( $_POST['email'] ?? '' );\n\n\u002F\u002F Sanitize URL for database storage\n$url = sanitize_url( $_POST['website'] ?? '' );\n\n\u002F\u002F Sanitize textarea\n$description = sanitize_textarea_field( $_POST['description'] ?? '' );\n```\n\n### Important notes on sanitization\n\n- **Never use escape functions for sanitization** - they serve different purposes\n- When using `filter_var()`, always specify a sanitizing filter (not `FILTER_DEFAULT`)\n- Process only the specific keys you need, not the entire `$_POST`\u002F`$_GET` array\n\n```php\n\u002F\u002F CORRECT: Specify sanitizing filter\n$post_id = filter_input( INPUT_GET, 'post_id', FILTER_SANITIZE_NUMBER_INT );\n\n\u002F\u002F WRONG: No filter or FILTER_DEFAULT does not sanitize\n$post_id = filter_input( INPUT_GET, 'post_id' ); \u002F\u002F Insecure!\n```\n\n## Validation\n\nValidation verifies data matches expected patterns. **Prefer validation over sanitization when possible.**\n\n### Validation philosophies\n\n#### Safelist (recommended)\n\nAccept only known, trusted values:\n\n```php\n$allowed_values = array( 'draft', 'pending', 'publish' );\n\n\u002F\u002F Use strict comparison (third parameter = true)\nif ( in_array( $status, $allowed_values, true ) ) {\n    \u002F\u002F Valid\n} else {\n    wp_die( 'Invalid status' );\n}\n```\n\n#### Format detection\n\nTest data format and reject if invalid:\n\n```php\n\u002F\u002F Check alphanumeric only\nif ( ! ctype_alnum( $data ) ) {\n    wp_die( 'Invalid format' );\n}\n\n\u002F\u002F Check against regex\nif ( ! preg_match( '\u002F^\\d{5}(-\\d{4})?$\u002F', $zip_code ) ) {\n    wp_die( 'Invalid ZIP code format' );\n}\n```\n\n#### Type checking\n\nAlways use strict comparison (`===`) to prevent type juggling attacks:\n\n```php\n\u002F\u002F CORRECT: Strict comparison\nif ( 1 === $user_input ) {\n    \u002F\u002F Exactly integer 1\n}\n\n\u002F\u002F WRONG: Loose comparison - \"1 malicious\" == 1 evaluates to true\nif ( 1 == $user_input ) {\n    \u002F\u002F Vulnerable!\n}\n```\n\n### Validation functions\n\n| Function | Purpose |\n|----------|---------|\n| `is_email()` | Validate email format |\n| `term_exists()` | Check if taxonomy term exists |\n| `username_exists()` | Check if username exists |\n| `validate_file()` | Validate file path (not existence) |\n| `is_array()` | Check if value is array |\n| `absint()` | Return absolute integer |\n| `in_array( $val, $arr, true )` | Check value in array (strict) |\n\n### Validation example\n\n```php\nfunction ayudawp_is_valid_us_zip( string $zip ): bool {\n    if ( empty( $zip ) ) {\n        return false;\n    }\n\n    if ( strlen( trim( $zip ) ) > 10 ) {\n        return false;\n    }\n\n    if ( ! preg_match( '\u002F^\\d{5}(-?\\d{4})?$\u002F', $zip ) ) {\n        return false;\n    }\n\n    return true;\n}\n\n\u002F\u002F Usage\nif ( isset( $_POST['zip'] ) && ayudawp_is_valid_us_zip( $_POST['zip'] ) ) {\n    $zip = sanitize_text_field( $_POST['zip'] );\n    \u002F\u002F Process valid ZIP\n}\n```\n\n## Escaping\n\nEscape output data **as late as possible**, immediately when echoing.\n\n### Escaping functions\n\n| Function | Use case |\n|----------|----------|\n| `esc_html()` | Text inside HTML elements |\n| `esc_attr()` | Values inside HTML attributes |\n| `esc_url()` | URLs in href, src attributes |\n| `esc_url_raw()` | URLs for database storage (NOT escaping) |\n| `esc_js()` | Inline JavaScript values |\n| `esc_textarea()` | Content inside textarea |\n| `esc_xml()` | XML content |\n| `wp_kses()` | HTML with custom allowed tags |\n| `wp_kses_post()` | HTML allowed in post content |\n| `wp_kses_data()` | HTML allowed in comments |\n\n### Escaping examples\n\n```php\n\u002F\u002F Text inside HTML element\n\u003Ch4>\u003C?php echo esc_html( $title ); ?>\u003C\u002Fh4>\n\n\u002F\u002F URL in attribute\n\u003Ca href=\"\u003C?php echo esc_url( $link ); ?>\">Link\u003C\u002Fa>\n\n\u002F\u002F Value in attribute\n\u003Cinput type=\"text\" value=\"\u003C?php echo esc_attr( $value ); ?>\">\n\n\u002F\u002F Image source\n\u003Cimg src=\"\u003C?php echo esc_url( $image_url ); ?>\" alt=\"\u003C?php echo esc_attr( $alt ); ?>\">\n\n\u002F\u002F Inline JavaScript\n\u003Cdiv onclick=\"doSomething('\u003C?php echo esc_js( $param ); ?>')\">\n\n\u002F\u002F Textarea content\n\u003Ctextarea>\u003C?php echo esc_textarea( $content ); ?>\u003C\u002Ftextarea>\n\n\u002F\u002F HTML content (preserves allowed HTML)\n\u003Cdiv>\u003C?php echo wp_kses_post( $html_content ); ?>\u003C\u002Fdiv>\n```\n\n### Escape late pattern\n\nAlways escape at the point of output:\n\n```php\n\u002F\u002F WRONG: Escaping early\n$url = esc_url( $url );\n$text = esc_html( $text );\necho '\u003Ca href=\"' . $url . '\">' . $text . '\u003C\u002Fa>';\n\n\u002F\u002F CORRECT: Escaping late\necho '\u003Ca href=\"' . esc_url( $url ) . '\">' . esc_html( $text ) . '\u003C\u002Fa>';\n```\n\n### Escaping with localization\n\nUse combined escape + localization functions:\n\n```php\n\u002F\u002F Escape + translate\necho esc_html__( 'Hello World', 'text-domain' );\nesc_html_e( 'Hello World', 'text-domain' );\n\n\u002F\u002F With context\necho esc_html_x( 'Post', 'noun', 'text-domain' );\n\n\u002F\u002F For attributes\necho esc_attr__( 'Submit', 'text-domain' );\nesc_attr_e( 'Submit', 'text-domain' );\n```\n\nAvailable combined functions:\n- `esc_html__()`, `esc_html_e()`, `esc_html_x()`\n- `esc_attr__()`, `esc_attr_e()`, `esc_attr_x()`\n\n### Important escaping notes\n\n- **Never use `__()` or `_e()` without escaping** - they do not escape output\n- **`esc_url_raw()` is NOT an escaping function** - it's for sanitizing URLs for storage\n- Use `wp_kses_post()` or `wp_kses()` for HTML output, NOT `esc_html()` which strips HTML\n- When escaping HTML attributes, escape the entire value, not parts\n\n```php\n\u002F\u002F CORRECT: Escape the whole attribute value\necho '\u003Cdiv id=\"' . esc_attr( $prefix . '-box-' . $id ) . '\">';\n\n\u002F\u002F WRONG: Escaping parts separately\necho '\u003Cdiv id=\"' . esc_attr( $prefix ) . '-box-' . esc_attr( $id ) . '\">';\n```\n\n### Custom HTML escaping with wp_kses\n\n```php\n$allowed_html = array(\n    'a'      => array(\n        'href'  => array(),\n        'title' => array(),\n    ),\n    'br'     => array(),\n    'em'     => array(),\n    'strong' => array(),\n);\n\necho wp_kses( $user_html, $allowed_html );\n```\n\n## Nonces\n\nNonces protect against CSRF (Cross-Site Request Forgery) attacks.\n\n### Creating nonces\n\n```php\n\u002F\u002F In a URL\n$url = wp_nonce_url( $base_url, 'delete-post_' . $post_id );\n\n\u002F\u002F In a form (echoes hidden fields)\nwp_nonce_field( 'save-settings_' . $user_id, 'ayudawp_nonce' );\n\n\u002F\u002F Get nonce value only\n$nonce = wp_create_nonce( 'my-action_' . $post_id );\n```\n\n### Verifying nonces\n\n```php\n\u002F\u002F In admin screens (also checks referrer)\ncheck_admin_referer( 'delete-post_' . $post_id, 'ayudawp_nonce' );\n\n\u002F\u002F In AJAX requests\ncheck_ajax_referer( 'my-ajax-action', 'security' );\n\n\u002F\u002F Manual verification\nif ( ! wp_verify_nonce( \n    sanitize_text_field( wp_unslash( $_POST['ayudawp_nonce'] ?? '' ) ), \n    'my-action_' . $post_id \n) ) {\n    wp_die( 'Security check failed' );\n}\n```\n\n### Nonce best practices\n\n- Make action strings specific: `'delete-post_' . $post_id` not just `'delete'`\n- Always sanitize nonce before verification:\n\n```php\n\u002F\u002F CORRECT: Sanitize nonce input\nif ( ! isset( $_POST['_wpnonce'] ) || \n     ! wp_verify_nonce( \n         sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), \n         'my_action' \n     ) \n) {\n    wp_die( 'Security check failed' );\n}\n```\n\n- Nonces have limited lifetime (default 24 hours, configurable)\n- **Nonces alone are not sufficient** - always combine with capability checks\n- Nonces are user-specific and session-specific\n\n### Modifying nonce lifetime\n\n```php\nadd_filter( 'nonce_life', function() {\n    return 4 * HOUR_IN_SECONDS;\n} );\n```\n\n## User capabilities\n\nAlways verify user has permission before performing actions.\n\n### Checking capabilities\n\n```php\n\u002F\u002F Check current user capability\nif ( ! current_user_can( 'edit_posts' ) ) {\n    wp_die( 'You do not have permission to do this.' );\n}\n\n\u002F\u002F Check capability for specific post\nif ( ! current_user_can( 'edit_post', $post_id ) ) {\n    wp_die( 'You cannot edit this post.' );\n}\n\n\u002F\u002F Check if user is admin\nif ( ! current_user_can( 'manage_options' ) ) {\n    wp_die( 'Administrator access required.' );\n}\n```\n\n### Common capabilities\n\n| Capability | Role level |\n|------------|------------|\n| `read` | Subscriber+ |\n| `edit_posts` | Contributor+ |\n| `publish_posts` | Author+ |\n| `edit_others_posts` | Editor+ |\n| `manage_options` | Administrator |\n| `edit_themes` | Administrator |\n| `activate_plugins` | Administrator |\n\n### Complete security check example\n\n```php\nfunction ayudawp_delete_item() {\n    \u002F\u002F 1. Check nonce\n    if ( ! isset( $_POST['_wpnonce'] ) ||\n         ! wp_verify_nonce( \n             sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), \n             'delete_item_' . absint( $_POST['item_id'] ?? 0 )\n         )\n    ) {\n        wp_die( 'Security check failed' );\n    }\n\n    \u002F\u002F 2. Check capability\n    if ( ! current_user_can( 'delete_posts' ) ) {\n        wp_die( 'You do not have permission to delete items.' );\n    }\n\n    \u002F\u002F 3. Validate and sanitize input\n    $item_id = absint( $_POST['item_id'] ?? 0 );\n    if ( ! $item_id ) {\n        wp_die( 'Invalid item ID' );\n    }\n\n    \u002F\u002F 4. Perform action\n    \u002F\u002F ... delete logic here\n}\n```\n\n## SQL injection prevention\n\n### Use $wpdb->prepare()\n\nAlways use prepared statements for database queries:\n\n```php\nglobal $wpdb;\n\n\u002F\u002F Single value\n$result = $wpdb->get_var( \n    $wpdb->prepare(\n        \"SELECT post_title FROM {$wpdb->posts} WHERE ID = %d\",\n        $post_id\n    )\n);\n\n\u002F\u002F Multiple values\n$results = $wpdb->get_results(\n    $wpdb->prepare(\n        \"SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_author = %d\",\n        $status,\n        $author_id\n    )\n);\n```\n\n### Placeholders\n\n| Placeholder | Type |\n|-------------|------|\n| `%d` | Integer |\n| `%f` | Float |\n| `%s` | String |\n| `%i` | Identifier (table\u002Fcolumn name, WP 6.2+) |\n\n### Arrays in queries\n\n```php\n\u002F\u002F Build placeholders for array\n$ids = array( 1, 2, 3, 4, 5 );\n$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );\n\n$results = $wpdb->get_results(\n    $wpdb->prepare(\n        \"SELECT * FROM {$wpdb->posts} WHERE ID IN ( $placeholders )\",\n        $ids\n    )\n);\n```\n\n### Use WordPress functions when possible\n\nPrefer WordPress API functions over direct SQL:\n\n```php\n\u002F\u002F PREFERRED: Use WordPress functions\nupdate_post_meta( $post_id, 'my_key', $value );\nget_option( 'my_option' );\nWP_Query for post queries\n\n\u002F\u002F AVOID: Direct SQL unless necessary\n$wpdb->query( \"INSERT INTO...\" );\n```\n\n## Common vulnerabilities\n\n### XSS (Cross-Site Scripting)\n\n**Prevention**: Escape all output\n\n```php\n\u002F\u002F Vulnerable\necho $user_input;\n\n\u002F\u002F Secure\necho esc_html( $user_input );\n```\n\n### CSRF (Cross-Site Request Forgery)\n\n**Prevention**: Use nonces + capability checks\n\n```php\n\u002F\u002F In form\nwp_nonce_field( 'my_action', 'my_nonce' );\n\n\u002F\u002F On submission\ncheck_admin_referer( 'my_action', 'my_nonce' );\nif ( ! current_user_can( 'manage_options' ) ) {\n    wp_die( 'Unauthorized' );\n}\n```\n\n### SQL Injection\n\n**Prevention**: Use prepared statements\n\n```php\n\u002F\u002F Vulnerable\n$wpdb->query( \"DELETE FROM table WHERE id = \" . $_GET['id'] );\n\n\u002F\u002F Secure\n$wpdb->query( \n    $wpdb->prepare( \"DELETE FROM table WHERE id = %d\", absint( $_GET['id'] ) )\n);\n```\n\n## File handling security\n\n### Use WordPress upload functions\n\n```php\n\u002F\u002F CORRECT: Use wp_handle_upload\n$uploaded = wp_handle_upload( $_FILES['my_file'], array( \n    'test_form' => false \n) );\n\n\u002F\u002F WRONG: Direct move_uploaded_file\nmove_uploaded_file( $_FILES['my_file']['tmp_name'], $destination );\n```\n\n### Never allow unfiltered uploads\n\n```php\n\u002F\u002F NEVER DO THIS\ndefine( 'ALLOW_UNFILTERED_UPLOADS', true );\n\n\u002F\u002F Instead, use upload_mimes filter for specific file types\nadd_filter( 'upload_mimes', function( $mimes ) {\n    $mimes['svg'] = 'image\u002Fsvg+xml';\n    return $mimes;\n} );\n```\n\n### Validate file types\n\n```php\n$allowed_types = array( 'image\u002Fjpeg', 'image\u002Fpng', 'image\u002Fgif' );\n$file_type = wp_check_filetype( $filename );\n\nif ( ! in_array( $file_type['type'], $allowed_types, true ) ) {\n    wp_die( 'Invalid file type' );\n}\n```\n\n## Direct file access prevention\n\nAdd to all PHP files that could execute code:\n\n```php\n\u003C?php\n\u002F\u002F Prevent direct file access\nif ( ! defined( 'ABSPATH' ) ) {\n    exit;\n}\n```\n\n## AJAX security\n\n### Register AJAX handlers\n\n```php\n\u002F\u002F For logged-in users\nadd_action( 'wp_ajax_my_action', 'ayudawp_ajax_handler' );\n\n\u002F\u002F For non-logged-in users (if needed)\nadd_action( 'wp_ajax_nopriv_my_action', 'ayudawp_ajax_handler' );\n\nfunction ayudawp_ajax_handler() {\n    \u002F\u002F 1. Verify nonce\n    check_ajax_referer( 'my_ajax_nonce', 'security' );\n\n    \u002F\u002F 2. Check capabilities\n    if ( ! current_user_can( 'edit_posts' ) ) {\n        wp_send_json_error( 'Unauthorized', 403 );\n    }\n\n    \u002F\u002F 3. Sanitize input\n    $data = sanitize_text_field( $_POST['data'] ?? '' );\n\n    \u002F\u002F 4. Process and respond\n    wp_send_json_success( array( 'result' => $data ) );\n}\n```\n\n### JavaScript side\n\n```php\n\u002F\u002F Localize script with nonce\nwp_localize_script( 'my-script', 'myAjax', array(\n    'ajaxurl' => admin_url( 'admin-ajax.php' ),\n    'nonce'   => wp_create_nonce( 'my_ajax_nonce' ),\n) );\n```\n\n```javascript\n\u002F\u002F AJAX call\njQuery.post( myAjax.ajaxurl, {\n    action: 'my_action',\n    security: myAjax.nonce,\n    data: 'my data'\n}, function( response ) {\n    \u002F\u002F Handle response\n});\n```\n\n## REST API security\n\n```php\nregister_rest_route( 'myplugin\u002Fv1', '\u002Fitems', array(\n    'methods'             => 'POST',\n    'callback'            => 'ayudawp_create_item',\n    'permission_callback' => function() {\n        return current_user_can( 'edit_posts' );\n    },\n    'args'                => array(\n        'title' => array(\n            'required'          => true,\n            'sanitize_callback' => 'sanitize_text_field',\n            'validate_callback' => function( $value ) {\n                return ! empty( $value );\n            },\n        ),\n    ),\n) );\n```\n\n## Code review checklist\n\n### Input handling\n\n- [ ] All `$_POST`, `$_GET`, `$_REQUEST` values are sanitized\n- [ ] All `$_FILES` uploads use `wp_handle_upload()`\n- [ ] Database queries use `$wpdb->prepare()`\n- [ ] Type casting used where appropriate (`absint()`, `(int)`, etc.)\n\n### Output handling\n\n- [ ] All dynamic output is escaped\n- [ ] Correct escape function used for context (html\u002Fattr\u002Furl\u002Fjs)\n- [ ] Escaping happens at output time (late escaping)\n- [ ] Translation functions are escaped (`esc_html__()` not `__()`)\n\n### Authentication & authorization\n\n- [ ] Nonces used on all forms and state-changing URLs\n- [ ] Nonces verified before processing actions\n- [ ] Capability checks performed before actions\n- [ ] Both nonce AND capability checked (not just one)\n\n### General\n\n- [ ] No `ALLOW_UNFILTERED_UPLOADS`\n- [ ] Direct file access prevented with `ABSPATH` check\n- [ ] No `error_reporting()` in production code\n- [ ] No timezone changes with `date_default_timezone_set()`\n- [ ] Uses WordPress HTTP API, not raw cURL\n- [ ] Uses `wp_enqueue_*` for scripts\u002Fstyles\n\n## WPCS security sniffs\n\nWordPress Coding Standards includes these security sniffs:\n\n- `EscapeOutputSniff` - Verifies output is escaped\n- `NonceVerificationSniff` - Verifies nonce checks\n- `ValidatedSanitizedInputSniff` - Verifies input sanitization\n- `SafeRedirectSniff` - Verifies safe redirects\n- `PluginMenuSlugSniff` - Verifies menu slug safety\n\nRun PHPCS with WordPress standards:\n\n```bash\nphpcs --standard=WordPress path\u002Fto\u002Fplugin\n```\n\n## References\n\n- [WordPress Security API](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002F)\n- [Escaping Data](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fescaping\u002F)\n- [Sanitizing Data](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fsanitizing\u002F)\n- [Data Validation](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fdata-validation\u002F)\n- [Nonces](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fnonces\u002F)\n- [User Roles and Capabilities](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fuser-roles-and-capabilities\u002F)\n- [Common Vulnerabilities](https:\u002F\u002Fdeveloper.wordpress.org\u002Fapis\u002Fsecurity\u002Fcommon-vulnerabilities\u002F)\n- [Plugin Review Team Common Issues](https:\u002F\u002Fdeveloper.wordpress.org\u002Fplugins\u002Fwordpress-org\u002Fcommon-issues\u002F)\n- [WordPress Coding Standards](https:\u002F\u002Fgithub.com\u002FWordPress\u002FWordPress-Coding-Standards)\n\n",{"name":5,"license":54,"metadata":55,"description":7,"compatibility":58},"GPL-2.0-or-later",{"author":56,"version":57},"fernando-tellado","1.0","WordPress 6.0+ \u002F PHP 7.4+. Applies to plugins, themes, and custom code.","main","def8531e65114c0fca8fb8551c1871ee0eed705c","skills\u002Ffernandotellado\u002Fai-skills\u002Fwp-plugin-security",24,"https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub","https:\u002F\u002Fskillshub.wtf","https:\u002F\u002Fgithub.com\u002FComeOnOliver","AI & Blockchain Engineer\r\n","2026-06-24 22:07:19",{"source":8,"skillName":5,"commands":69,"prompt":79,"zipAvailable":80},[70,73,76],{"tool":71,"command":72},"npx","npx skills add ComeOnOliver\u002Fskillshub --skill wp-plugin-security",{"tool":74,"command":75},"bunx","bunx skills add ComeOnOliver\u002Fskillshub --skill wp-plugin-security",{"tool":77,"command":78},"pnpm","pnpm dlx skills add ComeOnOliver\u002Fskillshub --skill wp-plugin-security","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Ffernandotellado\u002Fai-skills\u002Fwp-plugin-security\u002FSKILL.md",true,[],[83,96,106,115,121,128],{"skillNo":84,"name":85,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":92,"publishedAt":49},"01M6FKJN13GMG4KY57J3QD8GY5","model-hierarchy-skill","MIT","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzscole\u002Fmodel-hierarchy-skill\u002Fmodel-hierarchy-skill\u002FSKILL.md",[89],{"categoryNo":90,"categoryName":91,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[93,94,95],"model","model hierarchy","model hierarchy skill",{"skillNo":97,"name":98,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":99,"categories":100,"keywords":102,"publishedAt":49},"01M6FKJJJ6GCAMR4PPPRD12M5X","zoom-mcp","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-mcp\u002FSKILL.md",[101],{"categoryNo":90,"categoryName":91,"taxonomyType":21,"taxonomyTypeDesc":22},[103,104,105],"MCP","zoom","zoom mcp",{"skillNo":107,"name":108,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":109,"categories":110,"keywords":111,"publishedAt":49},"01M6FKJE3KYT3KPEZMEEHYKA6K","zoom-apps-sdk","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fzoom-apps-sdk\u002FSKILL.md",[],[112,113,114,104],"sdk","zoom apps sdk","apps",{"skillNo":116,"name":117,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":49},"01M6FKJ9EDH9Q5VWDF8HAJRSP0","windows","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwindows\u002FSKILL.md",[],[117],{"skillNo":122,"name":123,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":127,"publishedAt":49},"01M6FKJ666BEEFC3AE9GJ72Q93","whiteboard","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwhiteboard\u002FSKILL.md",[126],{"categoryNo":90,"categoryName":91,"taxonomyType":21,"taxonomyTypeDesc":22},[123],{"skillNo":129,"name":130,"nameZh":6,"description":6,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":86,"licenseFileUrl":6,"sourceHtmlUrl":131,"categories":132,"keywords":133,"publishedAt":49},"01M6FKJ21WFCDGTJQ2Q91T9DVW","websockets","https:\u002F\u002Fgithub.com\u002FComeOnOliver\u002Fskillshub\u002Fblob\u002Fmain\u002Fskills\u002Fzoom\u002Fskills\u002Fwebsockets\u002FSKILL.md",[],[130]]