Claude Code Plugins

Community-maintained marketplace

Feedback

wp-security-review

@vapvarun/claude-backup
3
0

WordPress security audit and vulnerability analysis. Use when reviewing WordPress code for security issues, auditing themes/plugins for vulnerabilities, checking authentication/authorization, analyzing input validation, or detecting security anti-patterns, or when user mentions "security review", "security audit", "vulnerability", "XSS", "SQL injection", "CSRF", "nonce", "sanitize", "escape", "validate", "authentication", "authorization", "permissions", "capabilities", "hacked", or "malware".

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name wp-security-review
description WordPress security audit and vulnerability analysis. Use when reviewing WordPress code for security issues, auditing themes/plugins for vulnerabilities, checking authentication/authorization, analyzing input validation, or detecting security anti-patterns, or when user mentions "security review", "security audit", "vulnerability", "XSS", "SQL injection", "CSRF", "nonce", "sanitize", "escape", "validate", "authentication", "authorization", "permissions", "capabilities", "hacked", or "malware".

WordPress Security Review Skill

Overview

Systematic security code review for WordPress themes, plugins, and custom code. Core principle: Scan for critical vulnerabilities first (SQL injection, XSS, authentication bypass), then authorization issues, then hardening opportunities. Report with line numbers and severity levels.

When to Use

Use when:

  • Reviewing PR/code for WordPress theme or plugin security
  • User reports suspected hack, malware, or security breach
  • Auditing before public release or security certification
  • Checking authentication, authorization, or capability checks
  • Investigating suspicious code or backdoors

Don't use for:

  • Performance-only reviews (use wp-performance-review)
  • General PHP code review not specific to WordPress
  • Server/infrastructure security (focus is on code)

Code Review Workflow

  1. Identify file type and apply relevant checks below
  2. Scan for critical vulnerabilities first (SQLi, XSS, RCE, auth bypass)
  3. Check authorization issues (missing capability checks, IDOR)
  4. Note hardening opportunities (security headers, configuration)
  5. Report with line numbers using output format below

OWASP Top 10 WordPress Mapping

OWASP Risk WordPress Manifestation
A01 Broken Access Control Missing current_user_can(), direct file access, IDOR
A02 Cryptographic Failures Weak hashing, exposed secrets, insecure cookies
A03 Injection SQL injection, XSS, command injection, LDAP injection
A04 Insecure Design Logic flaws, race conditions, predictable tokens
A05 Security Misconfiguration Debug enabled, directory listing, default credentials
A06 Vulnerable Components Outdated plugins, known CVEs, abandoned libraries
A07 Auth Failures Weak passwords, session fixation, brute force
A08 Data Integrity Failures Insecure deserialization, missing integrity checks
A09 Logging Failures Missing audit trails, excessive error exposure
A10 SSRF Unvalidated URLs in wp_remote_get(), redirects

File-Type Specific Checks

Plugin/Theme PHP Files (functions.php, plugin.php, *.php)

Scan for:

  • $_GET, $_POST, $_REQUEST without sanitization → CRITICAL: Input validation
  • $wpdb->query() with string concatenation → CRITICAL: SQL injection
  • echo, print without escaping → CRITICAL: XSS vulnerability
  • Missing wp_verify_nonce() in form handlers → CRITICAL: CSRF
  • Missing current_user_can() before privileged actions → CRITICAL: Auth bypass
  • eval(), assert(), create_function() → CRITICAL: Code execution
  • unserialize() with user input → CRITICAL: Object injection
  • include, require with user input → CRITICAL: LFI/RFI

Database Operations

Scan for:

  • $wpdb->prepare() not used with variables → CRITICAL: SQL injection
  • esc_sql() used instead of prepare() → WARNING: Prefer prepare()
  • LIKE queries without $wpdb->esc_like() → WARNING: Wildcard injection
  • Direct table creation without dbDelta() → INFO: Schema management

AJAX & REST Handlers

Scan for:

  • wp_ajax_nopriv_* without rate limiting → WARNING: Abuse potential
  • Missing permission_callback in REST routes → CRITICAL: Auth bypass
  • 'permission_callback' => '__return_true' → WARNING: Public endpoint
  • Missing nonce in AJAX actions → CRITICAL: CSRF vulnerability

File Operations

Scan for:

  • file_get_contents(), file_put_contents() with user paths → CRITICAL: Path traversal
  • move_uploaded_file() without validation → CRITICAL: Arbitrary upload
  • Missing MIME type validation → WARNING: Upload bypass
  • unlink(), rmdir() with user input → CRITICAL: Arbitrary deletion

Authentication & Sessions

Scan for:

  • Custom authentication instead of wp_authenticate() → WARNING: Security bypass
  • wp_set_auth_cookie() without proper validation → CRITICAL: Auth bypass
  • Session handling outside WordPress → WARNING: Session fixation
  • Plain text password storage → CRITICAL: Credential exposure

External Requests

Scan for:

  • wp_remote_get() with user-supplied URL → CRITICAL: SSRF
  • Missing URL validation before requests → WARNING: SSRF potential
  • allow_redirects => true with external URLs → WARNING: Open redirect

Cron & Scheduled Tasks

Scan for:

  • Cron hook name same as internal do_action() in callback → CRITICAL: Infinite recursion (DoS)
  • wp_schedule_event() without wp_next_scheduled() check → WARNING: Duplicate events
  • Missing wp_clear_scheduled_hook() on deactivation → WARNING: Orphaned events
  • Long-running cron without set_time_limit() → WARNING: Timeout issues
  • Cron callbacks without try-catch → WARNING: Silent failures

Detection pattern for infinite recursion:

// Check if any cron hook name matches a do_action() call in its callback
// Example: wp_schedule_event( time(), 'hourly', 'my_hook' );
//          add_action( 'my_hook', 'callback' );
//          function callback() { do_action( 'my_hook' ); } // INFINITE LOOP!

Search Patterns for Quick Detection

# Critical: SQL Injection patterns
grep -rn '\$wpdb->query\s*(' . | grep -v 'prepare'
grep -rn '\$wpdb->get_' . | grep -v 'prepare'
grep -rn "esc_sql\s*(" .

# Critical: XSS patterns (unescaped output)
grep -rn 'echo\s*\$_' .
grep -rn 'print\s*\$_' .
grep -rn '<?=\s*\$' . | grep -v 'esc_'

# Critical: Missing nonce verification
grep -rn 'wp_ajax_' . | grep -l 'wp_ajax' | xargs grep -L 'wp_verify_nonce\|check_ajax_referer'

# Critical: Dangerous functions
grep -rn '\beval\s*(' .
grep -rn '\bassert\s*(' .
grep -rn 'create_function\s*(' .
grep -rn 'unserialize\s*(' .
grep -rn 'call_user_func.*\$_' .

# Critical: File inclusion vulnerabilities
grep -rn 'include.*\$_\|require.*\$_' .
grep -rn 'file_get_contents.*\$_' .

# Critical: Missing capability checks
grep -rn 'update_option\|delete_option' . | grep -v 'current_user_can'
grep -rn 'wp_delete_post\|wp_update_post' . | grep -v 'current_user_can'

# Warning: Unsafe input usage
grep -rn '\$_GET\[' . | grep -v 'sanitize_\|esc_\|intval\|absint'
grep -rn '\$_POST\[' . | grep -v 'sanitize_\|esc_\|intval\|absint'

# Warning: REST API without permissions
grep -rn 'register_rest_route' . | grep -v 'permission_callback'
grep -rn '__return_true.*permission_callback\|permission_callback.*__return_true' .

# Info: Debug/development code left in
grep -rn 'WP_DEBUG.*true' .
grep -rn 'error_reporting\|display_errors' .
grep -rn 'var_dump\|print_r\|debug_backtrace' .

Quick Reference: Security Anti-Patterns

SQL Injection

// ❌ CRITICAL: SQL injection via string concatenation.
$results = $wpdb->get_results(
    "SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'"
);

// ❌ CRITICAL: SQL injection via sprintf (still vulnerable).
$results = $wpdb->get_results(
    sprintf( "SELECT * FROM %s WHERE ID = %s", $wpdb->posts, $id )
);

// ✅ GOOD: Use $wpdb->prepare() for all variable data.
$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
        $title
    )
);

// ❌ WARNING: esc_sql() is not sufficient for complex queries.
$title = esc_sql( $_GET['title'] );
$wpdb->query( "SELECT * FROM wp_posts WHERE post_title = '$title'" );

// ✅ GOOD: Always use prepare(), even for "safe" looking queries.
$wpdb->get_var(
    $wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_author = %d",
        $user_id
    )
);

// ❌ WARNING: LIKE queries need esc_like() in addition to prepare().
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s", '%' . $search . '%' );

// ✅ GOOD: Use esc_like() for LIKE wildcards.
$wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
    '%' . $wpdb->esc_like( $search ) . '%'
);

Cross-Site Scripting (XSS)

// ❌ CRITICAL: Unescaped output - XSS vulnerability.
echo $_GET['search'];
echo $user_input;
echo $post->post_title; // Even "safe" data should be escaped.

// ✅ GOOD: Always escape output based on context.
echo esc_html( $_GET['search'] );           // HTML context.
echo esc_attr( $value );                     // HTML attributes.
echo esc_url( $url );                        // URLs.
echo esc_js( $string );                      // JavaScript strings.
echo wp_kses_post( $content );              // Allow safe HTML.

// ❌ CRITICAL: Unescaped in HTML attribute.
<input value="<?php echo $value; ?>">

// ✅ GOOD: Escape for attribute context.
<input value="<?php echo esc_attr( $value ); ?>">

// ❌ CRITICAL: Unescaped URL - JavaScript injection.
<a href="<?php echo $url; ?>">Link</a>

// ✅ GOOD: Validate and escape URLs.
<a href="<?php echo esc_url( $url ); ?>">Link</a>

// ❌ WARNING: wp_kses_post() in wrong context.
<input value="<?php echo wp_kses_post( $value ); ?>">

// ✅ GOOD: Use appropriate escaping for context.
<input value="<?php echo esc_attr( wp_strip_all_tags( $value ) ); ?>">

// ❌ CRITICAL: JSON output without escaping.
<script>var data = <?php echo json_encode( $data ); ?>;</script>

// ✅ GOOD: Use wp_json_encode() and proper escaping.
<script>var data = <?php echo wp_json_encode( $data ); ?>;</script>

Cross-Site Request Forgery (CSRF)

// ❌ CRITICAL: Form without nonce - CSRF vulnerable.
<form method="post" action="">
    <input type="submit" value="Delete">
</form>
<?php
if ( isset( $_POST['submit'] ) ) {
    delete_data();
}

// ✅ GOOD: Add nonce field and verify on submission.
<form method="post" action="">
    <?php wp_nonce_field( 'delete_action', 'delete_nonce' ); ?>
    <input type="submit" name="submit" value="Delete">
</form>
<?php
if ( isset( $_POST['submit'] ) ) {
    if ( ! wp_verify_nonce( $_POST['delete_nonce'], 'delete_action' ) ) {
        wp_die( 'Security check failed' );
    }
    delete_data();
}

// ❌ CRITICAL: AJAX handler without nonce verification.
add_action( 'wp_ajax_delete_item', 'handle_delete' );
function handle_delete() {
    $id = intval( $_POST['id'] );
    wp_delete_post( $id );
    wp_die();
}

// ✅ GOOD: Verify nonce in AJAX handlers.
add_action( 'wp_ajax_delete_item', 'handle_delete' );
function handle_delete() {
    check_ajax_referer( 'delete_item_nonce', 'security' );

    if ( ! current_user_can( 'delete_posts' ) ) {
        wp_send_json_error( 'Unauthorized' );
    }

    $id = intval( $_POST['id'] );
    wp_delete_post( $id );
    wp_send_json_success();
}

Authorization & Capability Checks

// ❌ CRITICAL: No capability check before privileged action.
function delete_all_posts() {
    $wpdb->query( "TRUNCATE TABLE {$wpdb->posts}" );
}

// ✅ GOOD: Always verify capabilities.
function delete_all_posts() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Unauthorized access' );
    }
    // ... proceed with action.
}

// ❌ CRITICAL: Checking wrong capability.
if ( current_user_can( 'read' ) ) {
    update_option( 'critical_setting', $value );
}

// ✅ GOOD: Use appropriate capability for the action.
if ( current_user_can( 'manage_options' ) ) {
    update_option( 'critical_setting', $value );
}

// ❌ CRITICAL: IDOR - no ownership verification.
function get_user_data() {
    $user_id = intval( $_GET['user_id'] );
    return get_user_meta( $user_id, 'private_data', true );
}

// ✅ GOOD: Verify the user owns the resource or has permission.
function get_user_data() {
    $user_id = intval( $_GET['user_id'] );

    if ( get_current_user_id() !== $user_id && ! current_user_can( 'edit_users' ) ) {
        wp_die( 'Unauthorized access' );
    }

    return get_user_meta( $user_id, 'private_data', true );
}

// ❌ WARNING: REST endpoint with no permission check.
register_rest_route( 'myplugin/v1', '/data', array(
    'methods'  => 'GET',
    'callback' => 'get_sensitive_data',
) );

// ✅ GOOD: Always define permission_callback.
register_rest_route( 'myplugin/v1', '/data', array(
    'methods'             => 'GET',
    'callback'            => 'get_sensitive_data',
    'permission_callback' => function() {
        return current_user_can( 'edit_posts' );
    },
) );

Input Validation & Sanitization

// ❌ CRITICAL: Using raw input directly.
$email = $_POST['email'];
$name  = $_GET['name'];

// ✅ GOOD: Sanitize all input based on expected type.
$email = sanitize_email( $_POST['email'] );
$name  = sanitize_text_field( $_GET['name'] );
$html  = wp_kses_post( $_POST['content'] );
$int   = absint( $_GET['id'] );
$url   = esc_url_raw( $_POST['website'] );

// ❌ WARNING: Sanitizing but not validating.
$email = sanitize_email( $_POST['email'] );
update_user_meta( $user_id, 'email', $email );

// ✅ GOOD: Validate after sanitizing.
$email = sanitize_email( $_POST['email'] );
if ( ! is_email( $email ) ) {
    wp_die( 'Invalid email address' );
}
update_user_meta( $user_id, 'email', $email );

// ❌ CRITICAL: Trusting hidden form fields.
$user_role = $_POST['user_role']; // User can modify this!

// ✅ GOOD: Validate against allowed values.
$allowed_roles = array( 'subscriber', 'contributor' );
$user_role     = sanitize_text_field( $_POST['user_role'] );
if ( ! in_array( $user_role, $allowed_roles, true ) ) {
    wp_die( 'Invalid role' );
}

// ❌ WARNING: Using sanitize_text_field for file paths.
$file = sanitize_text_field( $_GET['file'] );
include $file;

// ✅ GOOD: Validate file paths against whitelist.
$allowed_files = array( 'header.php', 'footer.php' );
$file          = basename( $_GET['file'] ); // Strip path traversal.
if ( ! in_array( $file, $allowed_files, true ) ) {
    wp_die( 'Invalid file' );
}
include get_template_directory() . '/' . $file;

File Upload Security

// ❌ CRITICAL: No validation on file upload.
$target = wp_upload_dir()['path'] . '/' . $_FILES['file']['name'];
move_uploaded_file( $_FILES['file']['tmp_name'], $target );

// ✅ GOOD: Use WordPress upload handling with validation.
$allowed_types = array( 'image/jpeg', 'image/png', 'image/gif' );

// Verify MIME type.
$file_type = wp_check_filetype( $_FILES['file']['name'] );
if ( ! in_array( $file_type['type'], $allowed_types, true ) ) {
    wp_die( 'Invalid file type' );
}

// Use WordPress media handling.
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/image.php';

$attachment_id = media_handle_upload( 'file', 0 );
if ( is_wp_error( $attachment_id ) ) {
    wp_die( $attachment_id->get_error_message() );
}

// ❌ CRITICAL: Extension-only check (bypassable).
$ext = pathinfo( $_FILES['file']['name'], PATHINFO_EXTENSION );
if ( $ext === 'jpg' ) {
    // Allows "malware.php.jpg" renamed to "malware.php".
}

// ✅ GOOD: Check both extension and MIME type.
$file_info    = wp_check_filetype_and_ext(
    $_FILES['file']['tmp_name'],
    $_FILES['file']['name']
);
$allowed_mimes = array( 'jpg|jpeg|jpe' => 'image/jpeg' );
if ( ! in_array( $file_info['type'], $allowed_mimes, true ) ) {
    wp_die( 'Invalid file' );
}

Dangerous Functions

// ❌ CRITICAL: Code execution via eval().
eval( $_POST['code'] );

// ❌ CRITICAL: Code execution via assert().
assert( $_GET['assertion'] );

// ❌ CRITICAL: Deprecated and dangerous.
create_function( '$a', $_POST['code'] );

// ❌ CRITICAL: Object injection via unserialize().
$data = unserialize( $_COOKIE['data'] );

// ✅ GOOD: Use JSON for data serialization.
$data = json_decode( $_COOKIE['data'], true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
    $data = array();
}

// ❌ CRITICAL: Command injection.
system( 'ls ' . $_GET['dir'] );
exec( $_POST['command'] );
shell_exec( $user_input );
passthru( $command );

// ✅ GOOD: Avoid shell commands; if necessary, escape properly.
$safe_dir = escapeshellarg( $dir );
$output   = shell_exec( "ls $safe_dir" );

// ❌ CRITICAL: Dynamic function calls with user input.
$func = $_GET['callback'];
$func(); // Arbitrary function execution!
call_user_func( $_POST['function'], $args );

// ✅ GOOD: Whitelist allowed functions.
$allowed_callbacks = array(
    'format_date'   => 'my_format_date',
    'format_number' => 'my_format_number',
);
$callback_key = sanitize_key( $_GET['callback'] );
if ( isset( $allowed_callbacks[ $callback_key ] ) ) {
    call_user_func( $allowed_callbacks[ $callback_key ], $args );
}

Server-Side Request Forgery (SSRF)

// ❌ CRITICAL: SSRF - user controls URL destination.
$url      = $_GET['url'];
$response = wp_remote_get( $url );

// ✅ GOOD: Validate URL against whitelist.
$url         = esc_url_raw( $_GET['url'] );
$allowed     = array( 'api.example.com', 'cdn.example.com' );
$parsed_host = wp_parse_url( $url, PHP_URL_HOST );

if ( ! in_array( $parsed_host, $allowed, true ) ) {
    wp_die( 'URL not allowed' );
}

$response = wp_remote_get( $url, array(
    'timeout'     => 5,
    'redirection' => 0, // Disable redirects to prevent bypass.
) );

// ❌ WARNING: Allowing redirects can bypass host validation.
wp_remote_get( $url, array( 'redirection' => 5 ) );

// ✅ GOOD: Disable redirects or re-validate after redirect.
wp_remote_get( $url, array( 'redirection' => 0 ) );

Information Disclosure

// ❌ WARNING: Exposing debug info in production.
if ( WP_DEBUG ) {
    echo $wpdb->last_query;
    echo $wpdb->last_error;
}

// ✅ GOOD: Log errors instead of displaying.
if ( WP_DEBUG ) {
    error_log( $wpdb->last_error );
}

// ❌ WARNING: Exposing full paths.
wp_die( 'Error in ' . __FILE__ );

// ✅ GOOD: Generic error messages.
wp_die( 'An error occurred. Please try again.' );

// ❌ WARNING: Version exposure in headers/source.
<meta name="generator" content="WordPress <?php bloginfo( 'version' ); ?>">

// ✅ GOOD: Remove version information.
remove_action( 'wp_head', 'wp_generator' );

// ❌ WARNING: Exposing user enumeration.
// Accessible: /?author=1 redirects to /author/admin/

// ✅ GOOD: Block user enumeration.
add_action( 'template_redirect', function() {
    if ( isset( $_GET['author'] ) && ! is_admin() ) {
        wp_redirect( home_url(), 301 );
        exit;
    }
} );

Secure Cookies & Sessions

// ❌ WARNING: Cookie without security flags.
setcookie( 'user_pref', $value );

// ✅ GOOD: Set secure cookie flags.
setcookie(
    'user_pref',
    $value,
    array(
        'expires'  => time() + DAY_IN_SECONDS,
        'path'     => COOKIEPATH,
        'domain'   => COOKIE_DOMAIN,
        'secure'   => is_ssl(),
        'httponly' => true,
        'samesite' => 'Strict',
    )
);

// ❌ CRITICAL: Storing sensitive data in cookies.
setcookie( 'user_password', $password );
setcookie( 'api_key', $api_key );

// ✅ GOOD: Store sensitive data server-side, use session reference.
$session_token = wp_generate_password( 32, false );
set_transient( 'session_' . $session_token, $user_data, HOUR_IN_SECONDS );
setcookie( 'session_token', $session_token, /* secure flags */ );

Security Headers

// ✅ GOOD: Add security headers.
add_action( 'send_headers', function() {
    // Prevent clickjacking.
    header( 'X-Frame-Options: SAMEORIGIN' );

    // Prevent MIME type sniffing.
    header( 'X-Content-Type-Options: nosniff' );

    // Enable XSS filter.
    header( 'X-XSS-Protection: 1; mode=block' );

    // Referrer policy.
    header( 'Referrer-Policy: strict-origin-when-cross-origin' );

    // Content Security Policy (customize as needed).
    // header( "Content-Security-Policy: default-src 'self'" );
} );

Severity Definitions

Severity Description
Critical Direct exploitation possible (SQLi, XSS, RCE, auth bypass)
Warning Requires specific conditions to exploit
Info Security hardening opportunity

Output Format

Structure findings as:

## Security Review: [filename/component]

### Critical Vulnerabilities
- **Line X**: [Issue] - [Vulnerability type] - [Fix]

### Warnings
- **Line X**: [Issue] - [Risk] - [Fix]

### Hardening Recommendations
- [Security improvements]

### Summary
- Total issues: X Critical, Y Warnings, Z Info
- Risk level: [Critical/High/Medium/Low]
- Requires immediate attention: [Yes/No]

Common Mistakes

Mistake Why It's Wrong Fix
Using esc_sql() for injection protection Doesn't handle all cases Use $wpdb->prepare()
Escaping input instead of output Data may be used in multiple contexts Sanitize input, escape output
Nonce in GET request URL Nonces can be logged/cached Use POST for sensitive actions
Capability check in view, not controller Can be bypassed via direct request Check in action handler
Trusting is_admin() for security Only checks context, not permissions Use current_user_can()
Cron hook name = internal action name Infinite recursion, fatal error, DoS Use different names: my_cron vs my_do_cron
Not clearing cron on deactivation Orphaned events continue running Use wp_clear_scheduled_hook()
Checkbox not in sanitize callback Setting won't save when unchecked Explicitly set false for missing checkbox

Deep-Dive References

Load these references based on the task:

Task Reference to Load
Reviewing code for vulnerabilities references/vulnerabilities.md
Implementing authentication references/authentication-guide.md
Securing file operations references/file-security.md
Hardening configuration references/hardening-checklist.md