wp-security-review
WordPress security code review and vulnerability detection. Use when reviewing WordPress PHP code for security issues, auditing themes/plugins for vulnerabilities, checking code before launch, analyzing AJAX/REST handlers for exploits, detecting XSS, SQL injection, CSRF, or authorization bypass, or when user mentions "security review", "vulnerability", "XSS", "SQL injection", "CSRF", "nonce", "sanitization", "escaping", "capability check", "privilege escalation", "file upload security", "insecure code", "auth bypass", or "security audit". Detects vulnerabilities in input handling, output escaping, authorization, nonces, database queries, file uploads, and dangerous functions.
How do I install this agent skill?
npx skills add https://github.com/jorgerosal/wordpress-skills --skill wp-security-reviewIs this agent skill safe to install?
- Gen Agent Trust Hubpass
This skill provides a detailed framework for security auditing of WordPress code, identifying common vulnerabilities such as SQL injection, XSS, and CSRF. The skill is safe and instructional, with the only noted concern being a susceptibility to indirect prompt injection from the third-party code it is designed to analyze.
- Socketpass
No alerts
- Snykpass
Risk: LOW · No issues
What does this agent skill do?
WordPress Security Review Skill
Overview
Systematic security code review for WordPress themes, plugins, and custom code. Core principle: Three-pillar security model: (1) Sanitize input early, (2) Validate authorization (nonces + capabilities), (3) Escape output late. Scan critical issues first (SQL injection in public code, XSS on unescaped output, missing nonces on state-changing operations), then warnings, then info-level improvements. Report with line numbers, severity, CWE references, and BAD/GOOD code pairs.
When to Use
Use when:
- Security audit of WordPress theme or plugin
- Pre-launch security review
- Investigating reported vulnerability
- Reviewing form handlers, AJAX endpoints, or REST API routes
- Analyzing user input processing or output rendering
- Code review for exploitable patterns (XSS, SQL injection, CSRF, auth bypass)
Don't use for:
- Server hardening or infrastructure security (Apache/nginx config, firewall rules)
- Web Application Firewall (WAF) configuration
- SSL/TLS certificate management
- Performance-only audits (use wp-performance-review)
- General non-WordPress PHP security
Code Review Workflow
- Identify file type and apply relevant checks below
- Scan for CRITICAL patterns first (SQL injection in public code, XSS on unescaped output, missing nonces on state-changing operations, file upload without validation)
- Check WARNING patterns (authorization issues, admin-only XSS, incomplete validation)
- Note INFO improvements (defense-in-depth, security constants, hardening opportunities)
- Apply context-aware severity adjustment (admin-only code = lower severity, public-facing code = highest severity, WP-CLI/cron = nonces not needed, REST API = different auth model)
- Report with line numbers using output format below
File-Type Specific Checks
Plugin/Theme PHP Files (functions.php, plugin.php, *.php)
Scan for:
- Missing
defined( 'ABSPATH' ) || exit;at top → WARNING: Direct file access possible eval(→ CRITICAL: Code injection vector (CWE-95)exec(,shell_exec(,system(,passthru(→ CRITICAL: Command injection if user input present (CWE-78)base64_decode( $_→ CRITICAL: Possible encoded payload executionunserialize( $_→ CRITICAL: Object injection (CWE-502)$_GET[,$_POST[,$_REQUEST[withoutsanitize_*→ WARNING: Unsanitized input (CWE-20)include $_,require $_,include_once $_,require_once $_→ CRITICAL: Path traversal/inclusion (CWE-22)- Raw
move_uploaded_file(→ CRITICAL: Usewp_handle_upload()instead
Form Handlers (admin-post.php, admin_post_* hooks)
Scan for three-step pattern (ALL must be present):
- Nonce verification →
wp_verify_nonce( $_POST['nonce_field'], 'action_name' )orcheck_admin_referer()→ CRITICAL if missing (CWE-352) - Capability check →
current_user_can( 'capability' )→ CRITICAL if missing (CWE-862) - Input sanitization →
sanitize_text_field(),sanitize_email(), etc. → WARNING if missing (CWE-20)
Missing ANY step = vulnerability. State-changing operations MUST have all three.
AJAX Handlers (wp_ajax_*, wp_ajax_nopriv_* hooks)
Scan for:
check_ajax_referer( 'action_name', 'nonce_field' )→ CRITICAL if missing on state-changing operations (CWE-352)current_user_can( 'capability' )→ CRITICAL if missing when capability required (CWE-862)wp_ajax_nopriv_*without nonce → CRITICAL: Public endpoint with no CSRF protection- Input from
$_POSTwithoutsanitize_*→ WARNING: Unsanitized input (CWE-20) wp_send_json()without escaping when echoing user input → WARNING: JSON injection
REST API Endpoints (register_rest_route)
Scan for:
- Missing
permission_callback→ CRITICAL: WordPress 5.5+ requires this (CWE-862) 'permission_callback' => '__return_true'on write operations → CRITICAL: Allows unauthorized access'permission_callback' => '__return_true'on public read endpoints → OK (not a vulnerability)- Missing
current_user_can()in permission callback → WARNING: May allow privilege escalation (CWE-863) - Input validation missing on
$request->get_param()→ WARNING: Unsanitized input (CWE-20) - REST authentication via
X-WP-Nonceheader or application passwords → INFO: Verify client sends header
Database Code ($wpdb->query, $wpdb->get_results, etc.)
Scan for:
$wpdb->query(or$wpdb->get_results(with string concatenation or interpolation → CRITICAL: SQL injection (CWE-89)- Missing
$wpdb->prepare()when user input present → CRITICAL: SQL injection (CWE-89) $wpdb->prepare( "... LIKE '%{$term}%'" )→ WARNING: Should use$wpdb->esc_like()first- Double-preparing (preparing already-prepared strings) → WARNING: Escapes escape characters
- Direct use of
{$wpdb->prefix}in queries → OK (not a vulnerability, standard pattern)
Safe pattern: $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $post_id ) )
Template/Output Files (*.php in theme, templates)
Scan for:
echo $orprint $without escaping function → CRITICAL: XSS (CWE-79)<input value="<?php echo $→ CRITICAL: Useesc_attr()(CWE-79)<a href="<?php echo $→ CRITICAL: Useesc_url()(CWE-79)<script>blocks with PHP variables → CRITICAL: Useesc_js()orwp_localize_script()(CWE-79)- Rich HTML output without
wp_kses()orwp_kses_post()→ WARNING: May allow unsafe tags
Safe pattern: Always escape at point of output, not at input or storage.
File Upload Handlers
Scan for:
move_uploaded_file(instead ofwp_handle_upload()→ CRITICAL: Bypasses WP security checks (CWE-434)- Missing
current_user_can()check → CRITICAL: Unauthorized file upload (CWE-862) - No MIME type validation → WARNING: May allow executable uploads
- No file size limit → INFO: Could enable DoS
- Missing
wp_check_filetype_and_ext()→ WARNING: File type spoofing possible (CWE-434)
JavaScript Files (*.js, *.jsx)
Scan for:
fetch()or$.ajax()to REST API withoutX-WP-Nonceheader → WARNING: CSRF vulnerability (CWE-352)$.post( ajaxurl, ...)without nonce in data → WARNING: CSRF vulnerability (CWE-352)innerHTML = userInputordangerouslySetInnerHTML→ CRITICAL: DOM-based XSS (CWE-79)eval(→ CRITICAL: Code injection vector (CWE-95)
Search Patterns for Quick Detection (SEC-20)
# CRITICAL: SQL injection patterns
grep -rn "\$wpdb->query(" . | grep -v "prepare"
grep -rn "\$wpdb->get_results(" . | grep -v "prepare"
grep -rn "\$wpdb->get_var(" . | grep -v "prepare"
grep -rn "\$wpdb->get_row(" . | grep -v "prepare"
# CRITICAL: XSS (unescaped output)
grep -rn "echo \$_" .
grep -rn "print \$_" .
grep -rn "<?php echo \$" . | grep -v "esc_"
# CRITICAL: Dangerous functions
grep -rn "eval(" .
grep -rn "exec(" .
grep -rn "shell_exec(" .
grep -rn "system(" .
grep -rn "passthru(" .
grep -rn "base64_decode(\$_" .
grep -rn "unserialize(\$_" .
# CRITICAL: File upload without WP functions
grep -rn "move_uploaded_file(" .
# CRITICAL: REST API without permission_callback
grep -rn "register_rest_route" . | grep -v "permission_callback"
# CRITICAL: Path traversal / dynamic includes
grep -rn "include \$_" .
grep -rn "require \$_" .
grep -rn "include_once \$_" .
grep -rn "require_once \$_" .
# WARNING: Unsanitized input usage
grep -rn "\$_GET\[" . | grep -v "sanitize_"
grep -rn "\$_POST\[" . | grep -v "sanitize_"
grep -rn "\$_REQUEST\[" . | grep -v "sanitize_"
# WARNING: Missing nonces on public AJAX
grep -rn "wp_ajax_nopriv_" .
# WARNING: Missing capability checks
grep -rn "admin_post_" . | grep -v "current_user_can"
grep -rn "wp_ajax_" . | grep -v "current_user_can"
# INFO: Missing ABSPATH check
grep -rn "<?php" . | head -20 | grep -v "ABSPATH"
# INFO: Security constants missing (check wp-config.php)
grep -n "DISALLOW_FILE_EDIT" wp-config.php
grep -n "FORCE_SSL_ADMIN" wp-config.php
grep -n "DISALLOW_UNFILTERED_HTML" wp-config.php
Platform Context
Different hosting environments provide varying security layers:
Managed WordPress Hosts (WP Engine, Pantheon, Pressable, WordPress VIP, etc.):
- Often have platform-level WAF and malware scanning
- May block certain dangerous functions (eval, exec) at platform level
- Check host documentation for additional security requirements
- Code should still be written securely for portability
WordPress VIP:
- Strict code review requirements
- Many dangerous functions prohibited
- Additional security helpers available (
wpcom_vip_*functions) - See VIP documentation for required patterns
Self-Hosted / Standard Hosting:
- No platform-level security beyond what code implements
- More responsibility for secure coding practices
- Ensure all three pillars (sanitize, authorize, escape) are implemented
Quick Reference: Critical Security Patterns
XSS Prevention (Output Escaping)
// ❌ CRITICAL: Unescaped output (CWE-79)
<h1><?php echo $_GET['title']; ?></h1>
<input value="<?php echo $user_input; ?>">
<a href="<?php echo $url; ?>">Link</a>
<script>var data = "<?php echo $json; ?>";</script>
// ✅ GOOD: Context-appropriate escaping
<h1><?php echo esc_html( $_GET['title'] ); ?></h1>
<input value="<?php echo esc_attr( $user_input ); ?>">
<a href="<?php echo esc_url( $url ); ?>">Link</a>
<script>var data = <?php echo wp_json_encode( $data ); ?>;</script>
// ❌ CRITICAL: Early escaping (wrong pattern)
$title = esc_html( $_POST['title'] );
update_post_meta( $post_id, 'title', $title ); // Stores HTML entities
// ✅ GOOD: Late escaping (correct pattern)
$title = sanitize_text_field( $_POST['title'] );
update_post_meta( $post_id, 'title', $title );
echo '<h1>' . esc_html( get_post_meta( $post_id, 'title', true ) ) . '</h1>';
// ✅ GOOD: Rich HTML with wp_kses_post() for trusted content
echo wp_kses_post( $content ); // Allows safe HTML tags only
// ✅ GOOD: Custom allowed tags with wp_kses()
$allowed_tags = array(
'a' => array( 'href' => array(), 'title' => array() ),
'br' => array(),
'strong' => array(),
);
echo wp_kses( $user_content, $allowed_tags );
SQL Injection Prevention
// ❌ CRITICAL: Direct interpolation (CWE-89)
$results = $wpdb->get_results( "SELECT * FROM {$wpdb->users} WHERE ID = $user_id" );
$results = $wpdb->query( "UPDATE {$wpdb->posts} SET post_title = '$title'" );
// ✅ GOOD: Use $wpdb->prepare() with placeholders
$results = $wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE ID = %d",
$user_id
) );
$wpdb->query( $wpdb->prepare(
"UPDATE {$wpdb->posts} SET post_title = %s WHERE ID = %d",
$title,
$post_id
) );
// ✅ GOOD: Multiple placeholders
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_status = %s AND post_author = %d",
$status,
$author_id
) );
// ❌ WARNING: LIKE without esc_like()
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE '%%{$term}%%'" );
// ✅ GOOD: Use esc_like() for LIKE queries
$like_term = '%' . $wpdb->esc_like( $term ) . '%';
$wpdb->get_results( $wpdb->prepare(
"SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
$like_term
) );
// ✅ OK: Hardcoded SQL without user input (admin context)
if ( current_user_can( 'manage_options' ) ) {
$wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name = 'temp_setting'" );
}
CSRF/Nonce Verification (Three-Step Pattern)
// ❌ CRITICAL: Missing nonce verification (CWE-352)
add_action( 'admin_post_save_settings', function() {
update_option( 'my_setting', $_POST['value'] );
} );
// ❌ CRITICAL: Missing capability check (CWE-862)
add_action( 'admin_post_save_settings', function() {
check_admin_referer( 'save_settings_action', 'settings_nonce' );
update_option( 'my_setting', $_POST['value'] );
} );
// ✅ GOOD: Complete three-step pattern
add_action( 'admin_post_save_settings', function() {
// Step 1: Verify nonce
if ( ! isset( $_POST['settings_nonce'] ) ||
! wp_verify_nonce( $_POST['settings_nonce'], 'save_settings_action' ) ) {
wp_die( 'Invalid nonce' );
}
// Step 2: Check capability
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Insufficient permissions' );
}
// Step 3: Sanitize input
$value = sanitize_text_field( $_POST['value'] );
update_option( 'my_setting', $value );
wp_redirect( admin_url( 'admin.php?page=settings&updated=true' ) );
exit;
} );
// ✅ GOOD: Form with nonce field
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
<?php wp_nonce_field( 'save_settings_action', 'settings_nonce' ); ?>
<input type="hidden" name="action" value="save_settings">
<input type="text" name="value" value="<?php echo esc_attr( $current_value ); ?>">
<?php submit_button(); ?>
</form>
// ✅ GOOD: AJAX with nonce
add_action( 'wp_ajax_update_user_meta', function() {
check_ajax_referer( 'update_meta_nonce', 'nonce' );
if ( ! current_user_can( 'edit_users' ) ) {
wp_send_json_error( 'Insufficient permissions' );
}
$user_id = absint( $_POST['user_id'] );
$value = sanitize_text_field( $_POST['value'] );
update_user_meta( $user_id, 'custom_field', $value );
wp_send_json_success();
} );
// JavaScript for AJAX nonce
jQuery.post( ajaxurl, {
action: 'update_user_meta',
nonce: myAjax.nonce, // From wp_localize_script()
user_id: 123,
value: 'new value'
} );
Authorization/Capability Checks
// ❌ CRITICAL: No capability check (CWE-862)
add_action( 'admin_post_delete_user', function() {
wp_delete_user( $_POST['user_id'] );
} );
// ❌ WARNING: Incorrect capability (CWE-863)
add_action( 'admin_post_delete_user', function() {
if ( ! current_user_can( 'edit_posts' ) ) { // Wrong capability!
wp_die( 'Insufficient permissions' );
}
wp_delete_user( $_POST['user_id'] );
} );
// ✅ GOOD: Correct capability check
add_action( 'admin_post_delete_user', function() {
if ( ! current_user_can( 'delete_users' ) ) {
wp_die( 'Insufficient permissions' );
}
check_admin_referer( 'delete_user_action', 'delete_nonce' );
wp_delete_user( absint( $_POST['user_id'] ) );
} );
// ✅ GOOD: Check user can edit specific post
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( 'You cannot edit this post' );
}
// ✅ GOOD: REST API permission callback
register_rest_route( 'myapp/v1', '/users/(?P<id>\d+)', array(
'methods' => 'DELETE',
'callback' => 'myapp_delete_user',
'permission_callback' => function( $request ) {
return current_user_can( 'delete_users' );
},
) );
// ✅ OK: __return_true for public read endpoint
register_rest_route( 'myapp/v1', '/posts', array(
'methods' => 'GET',
'callback' => 'myapp_get_posts',
'permission_callback' => '__return_true', // Public read is OK
) );
File Upload Security
// ❌ CRITICAL: Direct move_uploaded_file() (CWE-434)
if ( isset( $_FILES['upload'] ) ) {
move_uploaded_file( $_FILES['upload']['tmp_name'], '/uploads/' . $_FILES['upload']['name'] );
}
// ❌ CRITICAL: No capability check (CWE-862)
$file = wp_handle_upload( $_FILES['upload'], array( 'test_form' => false ) );
// ✅ GOOD: Complete file upload security
add_action( 'admin_post_upload_file', function() {
// Step 1: Verify nonce
check_admin_referer( 'upload_file_action', 'upload_nonce' );
// Step 2: Check capability
if ( ! current_user_can( 'upload_files' ) ) {
wp_die( 'Insufficient permissions' );
}
// Step 3: Validate file exists
if ( ! isset( $_FILES['upload'] ) || UPLOAD_ERR_OK !== $_FILES['upload']['error'] ) {
wp_die( 'File upload failed' );
}
// Step 4: Use wp_handle_upload() (validates MIME type, extension)
require_once( ABSPATH . 'wp-admin/includes/file.php' );
$file = wp_handle_upload( $_FILES['upload'], array( 'test_form' => false ) );
if ( isset( $file['error'] ) ) {
wp_die( 'Upload error: ' . esc_html( $file['error'] ) );
}
// Step 5: Store file info
$attachment_id = wp_insert_attachment( array(
'post_title' => sanitize_file_name( $_FILES['upload']['name'] ),
'post_mime_type' => $file['type'],
'post_status' => 'inherit',
), $file['file'] );
} );
// ✅ GOOD: Restrict allowed MIME types
add_filter( 'upload_mimes', function( $mimes ) {
// Remove potentially dangerous types
unset( $mimes['exe'] );
unset( $mimes['php'] );
// Add custom allowed types if needed
$mimes['svg'] = 'image/svg+xml';
return $mimes;
} );
// ✅ GOOD: File type validation
$filetype = wp_check_filetype_and_ext( $_FILES['upload']['tmp_name'], $_FILES['upload']['name'] );
if ( ! $filetype['ext'] ) {
wp_die( 'Invalid file type' );
}
Object Injection Prevention
// ❌ CRITICAL: Unserialize user input (CWE-502)
$data = unserialize( $_POST['data'] );
// ❌ CRITICAL: Unserialize from cookie
$data = unserialize( $_COOKIE['cart_data'] );
// ✅ GOOD: Use JSON instead of serialize for user-facing data
$data = json_decode( $_POST['data'], true );
if ( JSON_ERROR_NONE !== json_last_error() ) {
wp_die( 'Invalid JSON' );
}
// ✅ OK: Unserialize trusted internal data only
$data = get_option( 'my_internal_setting' ); // Already unserialized by WP
$meta = get_post_meta( $post_id, 'my_field', true ); // Already unserialized by WP
Dangerous Functions
// ❌ CRITICAL: eval() with user input (CWE-95)
eval( $_POST['code'] );
// ❌ CRITICAL: exec() with user input (CWE-78)
exec( 'ls -la ' . $_GET['dir'] );
// ❌ CRITICAL: shell_exec() with user input
$output = shell_exec( 'grep ' . $_POST['search'] . ' file.txt' );
// ✅ GOOD: Avoid eval(), exec(), shell_exec() entirely in web context
// If absolutely necessary, whitelist input strictly
$allowed_dirs = array( 'uploads', 'cache' );
$dir = sanitize_key( $_GET['dir'] );
if ( in_array( $dir, $allowed_dirs, true ) ) {
exec( 'ls -la ' . escapeshellarg( $dir ), $output );
}
Direct File Access Prevention
// ❌ WARNING: Missing ABSPATH check
<?php
// Plugin code here without security check
// ✅ GOOD: ABSPATH check at top of every PHP file
<?php
defined( 'ABSPATH' ) || exit;
// Plugin code here
// ✅ GOOD: Alternative pattern
if ( ! defined( 'ABSPATH' ) ) {
die( 'Direct access not permitted.' );
}
Input Sanitization
// ❌ WARNING: No sanitization (CWE-20)
$title = $_POST['title'];
update_post_meta( $post_id, 'title', $title );
// ✅ GOOD: Sanitize based on expected data type
$title = sanitize_text_field( $_POST['title'] );
$email = sanitize_email( $_POST['email'] );
$url = sanitize_url( $_POST['url'] );
$filename = sanitize_file_name( $_POST['filename'] );
$key = sanitize_key( $_POST['key'] );
$int = absint( $_POST['count'] );
$float = floatval( $_POST['price'] );
$textarea = sanitize_textarea_field( $_POST['description'] );
// ✅ GOOD: Array of integers
$ids = array_map( 'absint', (array) $_POST['ids'] );
// ✅ GOOD: Validate against whitelist
$allowed_types = array( 'post', 'page', 'product' );
$type = sanitize_key( $_POST['type'] );
if ( ! in_array( $type, $allowed_types, true ) ) {
wp_die( 'Invalid type' );
}
Severity Definitions (SEC-22)
| Severity | Description |
|---|---|
| CRITICAL | Exploitable without authentication OR leads to data breach, RCE, or privilege escalation. Examples: SQL injection on public endpoint, XSS on unescaped output, missing nonce on admin action, file upload without capability check, eval() with user input |
| WARNING | Exploitable with authentication OR requires specific conditions. Examples: XSS in admin-only code, missing capability check on logged-in action, SQL injection in admin context, incomplete input sanitization |
| INFO | Defense-in-depth improvement or hardening opportunity. Examples: missing ABSPATH check, security constants not set in wp-config.php, missing security headers |
Context adjustments:
- Admin-only code: Lower severity by one level (CRITICAL → WARNING)
- WP-CLI/cron context: Nonces not required (no CSRF risk)
- REST API: Different auth model (permission_callback, not nonces)
- Public-facing code: Highest severity (no authentication barrier)
Output Format (SEC-23)
Group findings by FILE and severity level:
## Security Review: [filename]
### CRITICAL Issues
**Line X** | CWE-79 | XSS via unescaped output
- **Issue**: User input echoed directly without escaping
- **Risk**: Allows attackers to inject malicious JavaScript
- **Context**: Public-facing template (HIGH severity)
❌ **BAD:**
```php
<h1><?php echo $_GET['title']; ?></h1>
✅ GOOD:
<h1><?php echo esc_html( $_GET['title'] ); ?></h1>
WARNING Issues
Line Y | CWE-352 | Missing nonce verification
- Issue: Form handler missing wp_verify_nonce()
- Risk: State can be changed via CSRF attack
- Context: Admin-only (reduced severity)
[BAD/GOOD code pair]
INFO Recommendations
Line Z | Defense-in-depth | Missing ABSPATH check
- Add
defined( 'ABSPATH' ) || exit;at top of file
Summary
- Total: 1 CRITICAL, 1 WARNING, 1 INFO
- Risk level: HIGH (exploitable CRITICAL issue present)
- Immediate action: Fix line X (unescaped output)
## Common Mistakes (SEC-24)
When performing security reviews, avoid these false positives:
| Mistake | Why It's Wrong | Context |
|---------|----------------|---------|
| Flagging missing nonces in WP-CLI commands | WP-CLI runs server-side, no CSRF risk | Nonces only needed for HTTP requests |
| Flagging `wp_kses_post()` as insufficient escaping | `wp_kses_post()` is valid for trusted content with safe HTML | Allows `<p>`, `<a>`, `<strong>`, etc. - appropriate for post content |
| Flagging admin-only `$wpdb->query()` with hardcoded SQL | No user input = no injection risk | Review for user input presence, not just prepare() usage |
| Flagging `current_user_can()` inside REST `permission_callback` as redundant | This is the CORRECT location for capability checks in REST API | REST API auth model, not redundant |
| Flagging `esc_html( get_the_title() )` as double-escaping | WP core sanitizes on storage, but late escaping is still best practice | Not technically double-escaping, WP stores raw data |
| Flagging `update_option()` in admin context as vulnerability | Options API is safe for admin use | Check capability first, then update_option() is fine |
| Flagging REST API with `__return_true` permission on GET endpoints | Public read access is often intentional | Only flag on write operations (POST, PUT, DELETE) |
| Flagging `sanitize_callback` in `register_setting()` as missing sanitization | Settings API handles sanitization via callback | This is the correct pattern for Settings API |
## Security Constants Check (SEC-19)
Review `wp-config.php` for security hardening constants:
```php
// ✅ GOOD: Disable file editor (INFO-level recommendation)
define( 'DISALLOW_FILE_EDIT', true );
// ✅ GOOD: Force SSL for admin (INFO if SSL available)
define( 'FORCE_SSL_ADMIN', true );
// ✅ GOOD: Disable unfiltered HTML for all users including admins
define( 'DISALLOW_UNFILTERED_HTML', true );
// ✅ GOOD: Disable plugin/theme installation (high-security environments)
define( 'DISALLOW_FILE_MODS', true );
// ✅ GOOD: Custom authentication keys (CRITICAL if using defaults)
define( 'AUTH_KEY', 'put-unique-phrase-here' ); // Use https://api.wordpress.org/secret-key/1.1/salt/
define( 'SECURE_AUTH_KEY', 'put-unique-phrase-here' );
define( 'LOGGED_IN_KEY', 'put-unique-phrase-here' );
define( 'NONCE_KEY', 'put-unique-phrase-here' );
define( 'AUTH_SALT', 'put-unique-phrase-here' );
define( 'SECURE_AUTH_SALT', 'put-unique-phrase-here' );
define( 'LOGGED_IN_SALT', 'put-unique-phrase-here' );
define( 'NONCE_SALT', 'put-unique-phrase-here' );
Deep-Dive References
Load these references for comprehensive vulnerability patterns and examples:
| Task | Reference to Load |
|---|---|
| Comprehensive vulnerability catalog with CWE mappings | references/vulnerability-patterns.md |
| Output escaping patterns and context-specific functions | references/escaping-guide.md |
| Input sanitization patterns by data type | references/sanitization-guide.md |
| Authorization and capability check patterns | references/auth-patterns.md |
| Nonce generation, verification, and CSRF prevention | references/nonce-csrf-guide.md |
Note: For standard security reviews, this SKILL.md contains all patterns needed. Load references when you need comprehensive CWE catalogs, edge cases, or platform-specific security requirements (WordPress VIP, etc.).
How can the creator link this skill?
Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.
<a href="https://skillzs.dev/skills/jorgerosal/wordpress-skills/wp-security-review">View wp-security-review on skillZs</a>