Running automated static analysis on your WordPress plugin before release catches entire classes of vulnerabilities — SQL injection, reflected XSS, open redirects, and insecure unserialize() calls — that manual code review often misses. Two tools stand out for PHP/WordPress work: the open-source Semgrep with its WordPress rule-set, and the community-maintained PHPCS WordPress-Security sniff.
Problem: A WordPress plugin codebase has never been audited for security — SQL injections, nonce mismatches, missing capability checks, and file inclusion vulnerabilities accumulate silently without automated detection.
Solution: Run phpcs --standard=WordPress-Security using the WordPress Coding Standards ruleset to catch common patterns like missing $wpdb->prepare(), output unescaped with echo $var, and missing nonce verification. Supplement with Psalm or PHPStan using the WordPress stubs package for type-level analysis.
The commands below show how to install both tools, run them against a plugin directory, and interpret the most important findings. A sample vulnerable snippet and its fixed version are included.
# 1. Install Semgrep
pip install semgrep
# 2. Run WordPress-specific rules from the semgrep-rules community registry
semgrep --config "p/wordpress" /path/to/my-plugin/
# 3. Install PHP_CodeSniffer + WordPress Security sniffs
composer require --dev squizlabs/php_codesniffer wp-coding-standards/wpcs
./vendor/bin/phpcs --config-set installed_paths vendor/wp-coding-standards/wpcs
# 4. Run PHPCS with WordPress-Security ruleset
./vendor/bin/phpcs --standard=WordPress-Security /path/to/my-plugin/
<?php
// ❌ VULNERABLE: direct $_GET use in SQL, unsanitised output
add_action( 'wp_ajax_my_action', function () {
global $wpdb;
$id = $_GET['id']; // no sanitisation
$row = $wpdb->get_row( "SELECT * FROM {$wpdb->posts} WHERE ID = $id" );
echo $row->post_title; // no escaping
wp_die();
} );
// ✅ FIXED: nonce check, absint sanitisation, prepared statement, escaped output
add_action( 'wp_ajax_my_action', function () {
check_ajax_referer( 'my_action_nonce', 'nonce' );
global $wpdb;
$id = absint( $_GET['id'] ?? 0 );
$row = $wpdb->get_row(
$wpdb->prepare( "SELECT * FROM {$wpdb->posts} WHERE ID = %d", $id )
);
if ( $row ) {
echo esc_html( $row->post_title );
}
wp_die();
} );
NOTE: Semgrep's p/wordpress pack flags unsafe $_GET/$_POST in SQL and echo statements; PHPCS WordPress-Security additionally checks for missing nonce verification and direct database access — run both tools together for maximum coverage.