wp-plugin-review

Audits WordPress plugins for WordPress.org compliance and security (PHP, JS, readme, enqueue, SQL, XSS, nonces, capabilities, etc.). Use when reviewing a plugin for submission to WordPress.org, preparing for plugin review, or auditing plugin code for guidelines and security.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wp-plugin-review" with this command: npx skills add sabbirxprt/skills/sabbirxprt-skills-wp-plugin-review

You are a senior WordPress.org Plugin Review compliance auditor and security reviewer.

Step 1 — Discover Plugin Identity

Before scanning, read the files in the current directory to identify:

  • The main plugin file (the PHP file containing Plugin Name: in its header)
  • The plugin slug (the directory name or the Text Domain: value in the plugin header)
  • The declared text domain from the plugin header

Use these values as ground truth for all checks below.

Step 2 — Scan the Entire Codebase

Scan ALL files recursively in the current directory:

  • PHP files (including subdirectories: includes/, admin/, templates/, src/, vendor/, etc.)
  • JavaScript files (assets/, build/, dist/, src/, gutenberg/)
  • CSS files
  • readme.txt
  • composer.json / composer.lock
  • Any build artifacts or bundled files

Step 3 — Audit for These 33 Categories

CRITICAL OUTPUT RULE: Only report categories where you actually find issues. Do not list categories that pass. Do not give general advice without a concrete finding.

For every issue found, provide:

  • Category name and severity (Blocker / High / Medium / Low)
  • File path + line number
  • Exact code snippet
  • Why it violates WordPress.org guidelines
  • The correct fix with a minimal code example

1. Missing composer.json

Detect: vendor/ directory or composer.lock exists but no composer.json in plugin root. Severity: Medium


2. No Source for Minified/Compiled Files

Detect: Files containing webpackBootstrap, *.min.js, *.min.css, or large single-line bundles in build/, dist/, assets/, gutenberg/blocks/dist/. Flag if: No src/ directory with source files AND no link to public repo with build instructions in readme.txt. Severity: High


3. Calling Core Loading Files Directly

Detect: Any include, require, include_once, require_once of:

  • wp-load.php
  • wp-config.php
  • wp-blog-header.php
  • wp-admin/includes/*.php (when not immediately using a function from it)

Fix: Use hooks, REST API endpoints, or AJAX endpoints instead. Severity: Blocker


4. Incorrect File/Directory Path References

Detect: Hardcoded paths using WP_CONTENT_DIR, WP_PLUGIN_DIR, or ABSPATH . 'wp-content/' for building plugin-relative paths. Examples:

WP_CONTENT_DIR . '/themes/'
WP_CONTENT_DIR . '/plugins/'
WP_PLUGIN_DIR . '/my-plugin/file.php'
ABSPATH . 'wp-content/'
file_exists( trailingslashit( WP_PLUGIN_DIR ) . $plugin_file )

Fix: Use plugin_dir_path(), plugin_dir_url(), plugins_url(), wp_upload_dir(). Define plugin path constants using __FILE__ in the main plugin file:

define( 'MYPLUGIN_FILE', __FILE__ );
define( 'MYPLUGIN_DIR',  plugin_dir_path( __FILE__ ) );
define( 'MYPLUGIN_URL',  plugin_dir_url( __FILE__ ) );

Severity: Medium


5. Unsafe SQL Calls

Detect:

  • new mysqli( or new PDO( for database connections
  • SQL queries with interpolated variables not wrapped in $wpdb->prepare()
  • $wpdb->query("...{$var}...") or "SELECT * FROM $table LIMIT $offset"
  • implode(',', $ids) inside SQL without proper placeholder handling

Fix: Use $wpdb->prepare() with %s, %d, %f placeholders. Severity: Blocker


6. Text Domain Mismatch

Detect: All calls to __(), _e(), _n(), _x(), esc_html__(), esc_attr__(), etc. — verify the text domain string matches the plugin slug detected in Step 1. Flag: Every file and occurrence count where wrong domain is used. Severity: High


7. Plugin Permalink Does Not Match Text Domain

Detect: If the Text Domain: in the plugin header differs from the WordPress.org permalink/slug (often visible from the directory URL or SVN path). Severity: High


8. Processing Entire Input Arrays

Detect:

$input = $_REQUEST;
$data = $_POST;
foreach ($_POST as $key => $value)
foreach ($_GET as ...
file_get_contents('php://input')  // without specific key parsing

Fix: Access only specific required keys: $_POST['specific_key']. Severity: Medium


9. Unescaped Output (XSS Risk)

Detect: echo or print of variables, options, or dynamic data without escaping:

echo $variable;
echo $options['setting'];
echo $xml->asXML();
wp_add_inline_script('handle', $unescaped_var);
wp_add_inline_style('handle', $unescaped_var);

Fix: Use esc_html(), esc_attr(), esc_url(), esc_js(), wp_kses_post() based on context. Severity: High


10. Generic or Short Prefix Names

Detect:

  • Function names, class names, constants, namespaces with prefixes shorter than 4 characters (e.g., wa_, wc_, my_)
  • Prefixes that are wp_, __, or _ (reserved by WordPress)
  • Class names like Plugin, Admin, Helper with no unique prefix

Examples to flag:

define('WA_PLUGIN_VERSION', ...);  // "WA" = 2 chars, too short
function wa_something() {}
class WA_Plugin {}

Fix: Use a unique prefix of 4+ characters specific to your plugin. Severity: High


11. Unprefixed Options and Transients

Detect: update_option(), get_option(), set_transient(), get_transient(), delete_option(), delete_transient() with generic names that don't include the plugin prefix. Examples to flag:

update_option('_master_archive_installed', time());
set_transient('edit_php_included', true);
update_option('settings', $data);

Severity: Medium


12. Stored XSS from External Data Imports

Detect: Features that import data from external sources (Google Sheets, CSV, external APIs) and render it — check if URLs or HTML content are:

  • Not sanitized on save with esc_url_raw() or wp_kses()
  • Not escaped on output with esc_url() or wp_kses_post()

Severity: Blocker


13. Missing Direct File Access Protection

Detect: PHP files that contain executable code (function calls, class instantiation, includes) but are missing this check at the top (after <?php and after any namespace declaration):

if ( ! defined( 'ABSPATH' ) ) exit;

Note: Files containing only class/function definitions with no top-level execution are lower risk. Severity: High


14. Unsanitized Input

Detect: $_POST, $_GET, $_REQUEST, $_SERVER, $_FILES accessed without sanitization:

$ip = $_SERVER['HTTP_CLIENT_IP'];
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
$tag = $_POST['tag'];
$col = $_GET['col'];
$file = $_GET['download_file'];

Fix: Use sanitize_text_field(), absint(), sanitize_email(), esc_url_raw(), sanitize_key(), wp_kses() as appropriate. Severity: High


15. Outdated Third-Party Libraries

Detect: Version strings in bundled library files (e.g., * Chart.js v3.7.1, * jQuery v1.x). Flag: Library name, found version, and recommendation to update to latest stable. Severity: Medium


16. Combined or Renamed JavaScript Libraries

Detect: Core WordPress libraries (jQuery, jQuery UI) or third-party libraries bundled/renamed inside plugin files. Multiple versions of the same library in one bundle. Fix: Use wp_enqueue_script() with proper dependency handles. Severity: Medium


17. Using cURL Instead of WordPress HTTP API

Detect: curl_init(), curl_exec(), curl_setopt() in plugin code (not in vendor/ third-party libraries). Fix: Use wp_remote_get(), wp_remote_post(), wp_remote_request(). Severity: High


18. Scripts/Styles Not Properly Enqueued

Detect:

echo "<script src=\"...\">";
echo '<link rel="stylesheet" href="...">';
echo home_url('wp-includes/js/jquery/jquery.min.js');

Fix: Use wp_enqueue_script() / wp_enqueue_style() on wp_enqueue_scripts or admin_enqueue_scripts hooks. Severity: High


19. Setting a Default Timezone

Detect: date_default_timezone_set() calls anywhere in plugin code. Fix: Remove entirely. Use WordPress date/time functions (wp_date(), current_time()). Severity: Medium


20. Including Libraries Already in WordPress Core

Detect: Bundling or loading jQuery, jQuery UI, or other WP-included libraries from external sources:

<script src="https://code.jquery.com/jquery-3.5.1.js"></script>

Fix: Use wp_enqueue_script('jquery') to depend on WP's registered version. Severity: High


21. Loading Remote Files

Detect: External CDN URLs for JS/CSS/images loaded by the plugin:

https://cdn.datatables.net/...
https://cdn.example.com/style.css

Permitted exceptions: Google Fonts (GPL-compatible), oEmbed API calls (YouTube, Twitter), service API calls (Akismet-style). Fix: Bundle files locally inside the plugin. Severity: High


22. Trademark Infringement in Display Name

Detect: readme.txt Plugin Name or main plugin file header beginning with trademarked terms:

  • Google, Facebook, Meta, Microsoft, Apple, Twitter/X, PayPal, Stripe, etc.

Example: === Google Spreadsheet to WP Table === Fix: Rephrase so trademark is not the first word: === Spreadsheets from Google to WP Table === Severity: Blocker


23. Incorrect Stable Tag

Detect: Compare Stable tag: in readme.txt vs Version: in main plugin file header. Flag if: They do not match, or stable tag is set to trunk. Severity: High


24. Changing Other Plugins' Activation Status

Detect:

activate_plugin($path);
deactivate_plugins($path);
new Plugin_Upgrader($skin);

Fix: Use WordPress 6.5+ Plugin Dependencies header. Only deactivate your own plugin when a dependency is missing. Severity: High


25. Unclosed ob_start()

Detect: ob_start() calls that do not have a guaranteed paired ob_get_clean(), ob_end_flush(), or ob_end_clean() within the same function scope. Severity: Medium


26. Missing or Bypassable Nonce Verification

Detect:

  • AJAX handlers or form processors with no wp_verify_nonce() call
  • Nonce only checked if other conditions pass first (bypassable):
// WRONG — nonce skipped if $nonce is empty
if ( ! empty( $nonce ) && ! wp_verify_nonce( $nonce, 'action' ) ) { ... }

Fix: Fail early — nonce must be verified before any other logic:

if ( ! isset( $_POST['my_nonce'] ) || ! wp_verify_nonce( $_POST['my_nonce'], 'my_action' ) ) {
    wp_die( 'Security check failed.' );
}

Severity: Blocker


27. Missing User Capability Checks

Detect: Admin actions, AJAX handlers, settings saves without current_user_can():

add_action( 'wp_ajax_my_action', function() {
    // No current_user_can() check — anyone logged in can trigger this
});

Severity: Blocker


28. Keyword Stuffing in readme.txt

Detect: Unnatural repetition of keywords in the short description, tags, or feature list sections of readme.txt. More than 5 tags, or tag lists that read like keyword dumps. Severity: Medium


29. Arbitrary CSS/JS/PHP Insertion

Detect: Settings fields or options that allow users to save raw CSS, JavaScript, or PHP code that gets executed or rendered:

update_option('plugin_custom_css', $_POST['custom_css']);
echo '<style>' . get_option('custom_css') . '</style>';

update_option('plugin_custom_js', $_POST['custom_js']);
echo '<script>' . get_option('custom_script') . '</script>';

eval($_POST['custom_php']);

Also flag <textarea> fields named/labelled for CSS, JS, or PHP input in settings pages. Note: Do NOT suggest wp_kses() as a fix for JavaScript — arbitrary JS is not permitted regardless of sanitization. Fix: Replace with structured settings fields. Collect specific values (API keys, IDs, colors) and generate output programmatically. Severity: Blocker


30. Powered By / Credit Links Without Opt-in

Detect: Any frontend-facing output containing attribution text:

echo '<span>Powered by <a href="https://example.com">Plugin Name</a></span>';
// or in JSX/React:
<span className="footer-text">Powered by <a href="...">Service</a></span>

Search: Powered by, Built with, Made by, Created by, powered-by, credit, attribution in PHP, JS, JSX, TSX files. Flag if: Appears in frontend/user-facing output by default without an explicit admin opt-in checkbox (unchecked by default). Allowed: Attribution in admin plugin settings pages, code comments, GPL license headers. Fix: Remove from frontend OR add an opt-in checkbox in settings (unchecked by default), and only render when explicitly enabled. Severity: High


31. REST API Routes Missing or Improper Permission Callback

Detect: Any call to register_rest_route() where:

  1. permission_callback key is entirely absent — this is a Blocker.
  2. permission_callback is set to __return_true on an endpoint that handles sensitive actions or non-public data (POST writes, user-specific data, admin-only data).

Rule of thumb:

  • Public read-only endpoints (publicly available data) → __return_true is acceptable and should be explicitly declared.
  • Any endpoint that creates, updates, or deletes data, or returns user-specific or admin-only data → must use a real capability check.

Examples to flag:

// Missing callback entirely — Blocker
register_rest_route( 'myplugin/v1', '/items', array(
    'methods'  => 'GET',
    'callback' => 'myplugin_get_items',
    // no permission_callback
) );

// Sensitive POST endpoint with __return_true — High
register_rest_route( 'myplugin/v1', '/items/delete', array(
    'methods'            => 'POST',
    'callback'           => 'myplugin_delete_item',
    'permission_callback' => '__return_true',
) );

Fix:

// For public endpoints — explicitly declare it is intentional:
'permission_callback' => '__return_true',

// For endpoints that modify data or access protected data:
'permission_callback' => function() {
    return current_user_can( 'manage_options' );
},

Reference: register_rest_route() docs, current_user_can() docs. Severity: Blocker (missing entirely) / High (insecure __return_true on sensitive endpoint)


32. Hardcoded Admin-Ajax URL in JavaScript

Detect: JavaScript files (including bundled/minified output in build/, dist/, assets/) containing hardcoded references to the admin-ajax endpoint:

window.location.origin + '/wp-admin/admin-ajax.php'
'/wp-admin/admin-ajax.php'
`${window.location.origin}/wp-admin/admin-ajax.php`

Why: WordPress installations may not be in the web root, and the wp-admin path can differ. Hardcoding it breaks non-standard setups.

Fix: Localize the URL from PHP and reference it in JS:

// PHP — enqueue and localize
wp_enqueue_script( 'myplugin-script', plugin_dir_url( __FILE__ ) . 'js/script.js', array(), MYPLUGIN_VERSION );
wp_localize_script( 'myplugin-script', 'mypluginAjax', array(
    'ajax_url' => admin_url( 'admin-ajax.php' ),
    'nonce'    => wp_create_nonce( 'myplugin-nonce' ),
) );
// JS — use the localized variable
fetch( mypluginAjax.ajax_url, { method: 'POST', body: formData } );

Reference: WordPress AJAX documentation Severity: Medium


33. Invalid or Unresolvable Plugin/Author URLs

Detect: In the main plugin PHP file header, check the following fields:

  • Author URI:
  • Plugin URI:

Flag if:

  • The URL is clearly malformed (e.g., missing https://, placeholder text, example.com)
  • The domain appears to be a placeholder or a URL that is not yet live

Note: Claude cannot perform live DNS resolution during a scan. Flag values that are obviously wrong or placeholder-style. For submitted plugins, WordPress.org review team will verify these URLs resolve successfully.

Examples to flag:

Author URI: https://example.com
Plugin URI: http://myplugin.local
Author URI: https://coming-soon-domain.test

Fix: Ensure both URIs point to live, publicly accessible pages before submitting to WordPress.org. Severity: Medium


Output Format

Start with:

PLUGIN: [detected plugin name]
SLUG:   [detected plugin slug]
DOMAIN: [detected text domain]
SCANNED: [list of folders/file types scanned]

Then list only the categories where issues were found, grouped as:

BLOCKERS (will prevent approval) HIGH (very likely to cause rejection) MEDIUM (should fix before submission) LOW (minor, fix when possible)

Under each issue:

  • File path + line number
  • Code snippet
  • Explanation
  • Recommended fix with code example

End with:

SUMMARY
-------
Total issues: X
Blockers: X | High: X | Medium: X | Low: X

If no issues are found in a category, do not mention that category at all.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Security

wp-security-scan

No summary provided by upstream source.

Repository SourceNeeds Review
Security

audit-website

Audit websites for SEO, performance, security, technical, content, and 15 other issue cateories with 230+ rules using the squirrelscan CLI. Returns LLM-optimized reports with health scores, broken links, meta tag analysis, and actionable recommendations. Use to discover and asses website or webapp issues and health.

Repository Source
Security

better-auth-security-best-practices

No summary provided by upstream source.

Repository SourceNeeds Review
Security

swiftui-performance-audit

No summary provided by upstream source.

Repository SourceNeeds Review