| 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
- Identify file type and apply relevant checks below
- Scan for critical vulnerabilities first (SQLi, XSS, RCE, auth bypass)
- Check authorization issues (missing capability checks, IDOR)
- Note hardening opportunities (security headers, configuration)
- 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:
Search Patterns for Quick Detection
grep -rn '\$wpdb->query\s*(' . | grep -v 'prepare'
grep -rn '\$wpdb->get_' . | grep -v 'prepare'
grep -rn "esc_sql\s*(" .
grep -rn 'echo\s*\$_' .
grep -rn 'print\s*\$_' .
grep -rn '<?=\s*\$' . | grep -v 'esc_'
grep -rn 'wp_ajax_' . | grep -l 'wp_ajax' | xargs grep -L 'wp_verify_nonce\|check_ajax_referer'
grep -rn '\beval\s*(' .
grep -rn '\bassert\s*(' .
grep -rn 'create_function\s*(' .
grep -rn 'unserialize\s*(' .
grep -rn 'call_user_func.*\$_' .
grep -rn 'include.*\$_\|require.*\$_' .
grep -rn 'file_get_contents.*\$_' .
grep -rn 'update_option\|delete_option' . | grep -v 'current_user_can'
grep -rn 'wp_delete_post\|wp_update_post' . | grep -v 'current_user_can'
grep -rn '\$_GET\[' . | grep -v 'sanitize_\|esc_\|intval\|absint'
grep -rn '\$_POST\[' . | grep -v 'sanitize_\|esc_\|intval\|absint'
grep -rn 'register_rest_route' . | grep -v 'permission_callback'
grep -rn '__return_true.*permission_callback\|permission_callback.*__return_true' .
grep -rn .
grep -rn .
grep -rn .
Quick Reference: Security Anti-Patterns
SQL Injection
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'"
);
$results = $wpdb->get_results(
sprintf( "SELECT * FROM %s WHERE ID = %s", $wpdb->posts, $id )
);
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
$title
)
);
$title = esc_sql( $_GET['title'] );
$wpdb->query( "SELECT * FROM wp_posts WHERE post_title = '$title'" );
$wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_author = %d",
$user_id
)
);
$wpdb->( , . . );
->(
,
. ->( ) .
);
Cross-Site Scripting (XSS)
echo $_GET['search'];
echo $user_input;
echo $post->post_title;
echo esc_html( $_GET['search'] );
echo esc_attr( $value );
echo esc_url( $url );
echo esc_js( $string );
echo wp_kses_post( $content );
<input value="<?php echo $value; ?>">
<input value="<?php echo esc_attr( $value ); ?>">
<a href="<?php echo $url; ?>">Link</a>
<a href="<?php echo esc_url( $url ); ?>">Link</a>
<input value="<?php echo wp_kses_post( ); ?>">
<input value=>
<script> data = ( ); ;</script>
<script> data = ( ); ;</script>
Cross-Site Request Forgery (CSRF)
<form method="post" action="">
<input type="submit" value="Delete">
</form>
<?php
if ( isset( $_POST['submit'] ) ) {
delete_data();
}
<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();
}
add_action( 'wp_ajax_delete_item', 'handle_delete' );
function handle_delete() {
$id = intval( $_POST['id'] );
wp_delete_post( );
();
}
( , );
{
( , );
( ! ( ) ) {
( );
}
= ( [] );
( );
();
}
Authorization & Capability Checks
function delete_all_posts() {
$wpdb->query( "TRUNCATE TABLE {$wpdb->posts}" );
}
function delete_all_posts() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Unauthorized access' );
}
}
if ( current_user_can( 'read' ) ) {
update_option( 'critical_setting', $value );
}
if ( current_user_can( 'manage_options' ) ) {
update_option( 'critical_setting', $value );
}
function get_user_data() {
$user_id = intval( $_GET['user_id'] );
return get_user_meta( $user_id, 'private_data', true );
}
{
= ( [] );
( () !== && ! ( ) ) {
( );
}
( , , );
}
( , , (
=> ,
=> ,
) );
( , , (
=> ,
=> ,
=> function() {
( );
},
) );
Input Validation & Sanitization
$email = $_POST['email'];
$name = $_GET['name'];
$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'] );
$email = sanitize_email( $_POST['email'] );
update_user_meta( $user_id, 'email', $email );
$email = sanitize_email( $_POST['email'] );
if ( ! is_email( $email ) ) {
wp_die( 'Invalid email address' );
}
update_user_meta( , , );
= [];
= ( , );
= ( [] );
( ! ( , , ) ) {
( );
}
= ( [] );
;
= ( , );
= ( [] );
( ! ( , , ) ) {
( );
}
() . . ;
File Upload Security
$target = wp_upload_dir()['path'] . '/' . $_FILES['file']['name'];
move_uploaded_file( $_FILES['file']['tmp_name'], $target );
$allowed_types = array( 'image/jpeg', 'image/png', 'image/gif' );
$file_type = wp_check_filetype( $_FILES['file']['name'] );
if ( ! in_array( $file_type['type'], $allowed_types, true ) ) {
wp_die( 'Invalid file type' );
}
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( ->() );
}
= ( [][], PATHINFO_EXTENSION );
( === ) {
}
= (
[][],
[][]
);
= ( => );
( ! ( [], , ) ) {
( );
}
Dangerous Functions
eval( $_POST['code'] );
assert( $_GET['assertion'] );
create_function( '$a', $_POST['code'] );
$data = unserialize( $_COOKIE['data'] );
$data = json_decode( $_COOKIE['data'], true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
$data = array();
}
system( 'ls ' . $_GET['dir'] );
exec( $_POST['command'] );
shell_exec( $user_input );
passthru( $command );
$safe_dir = escapeshellarg( $dir );
$output = shell_exec( "ls " );
= [];
();
( [], );
= (
=> ,
=> ,
);
= ( [] );
( ( [ ] ) ) {
( [ ], );
}
Server-Side Request Forgery (SSRF)
$url = $_GET['url'];
$response = wp_remote_get( $url );
$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.
) );
wp_remote_get( $url, array( 'redirection' => 5 ) );
wp_remote_get( $url, array( 'redirection' => 0 ) );
Information Disclosure
if ( WP_DEBUG ) {
echo $wpdb->last_query;
echo $wpdb->last_error;
}
if ( WP_DEBUG ) {
error_log( $wpdb->last_error );
}
wp_die( 'Error in ' . __FILE__ );
wp_die( 'An error occurred. Please try again.' );
<meta name="generator" content="WordPress <?php bloginfo( 'version' ); ?>">
remove_action( 'wp_head', 'wp_generator' );
add_action( 'template_redirect', function() {
if ( isset( $_GET['author'] ) && ! is_admin() ) {
wp_redirect( home_url(), 301 );
exit;
}
} );
Secure Cookies & Sessions
setcookie( 'user_pref', $value );
setcookie(
'user_pref',
$value,
array(
'expires' => time() + DAY_IN_SECONDS,
'path' => COOKIEPATH,
'domain' => COOKIE_DOMAIN,
'secure' => is_ssl(),
'httponly' => true,
'samesite' => 'Strict',
)
);
setcookie( 'user_password', $password );
setcookie( 'api_key', $api_key );
$session_token = wp_generate_password( 32, false );
set_transient( 'session_' . $session_token, $user_data, HOUR_IN_SECONDS );
setcookie( 'session_token', $session_token, );
Security Headers
add_action( 'send_headers', function() {
header( 'X-Frame-Options: SAMEORIGIN' );
header( 'X-Content-Type-Options: nosniff' );
header( 'X-XSS-Protection: 1; mode=block' );
header( 'Referrer-Policy: strict-origin-when-cross-origin' );
} );
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 |