SQL Injection in WordPress Plugins: How to Avoid It

Published Plugin SEO WP Stats Team By WP Stats Team
SQL Injection in WordPress Plugins: How to Avoid It

SQL injection in WordPress plugins almost never happens because a developer typed raw SQL on purpose. It happens because a variable from $_GET, $_POST, or a REST API request got concatenated into a query string without going through $wpdb->prepare() first. That one missed step is responsible for a huge share of the critical vulnerabilities logged against WordPress.org plugins every year.

This is a practical breakdown of where it happens, what the vulnerable code actually looks like next to the fixed version, and the checklist to run before you submit anything to the plugin directory.

Why SQL Injection in WordPress Plugins Keeps Happening in 2026

WordPress core's database layer, $wpdb, is safe when used correctly. The vulnerability almost always lives in plugin or theme code that bypasses the safe methods — usually in a custom admin report, a search filter, an AJAX handler, or a shortcode that queries a custom table.

  • WPScan's vulnerability database consistently lists SQL injection among the top three vulnerability classes in WordPress plugins, year after year.
  • Most cases involve plugins with their own custom database tables — think form plugins storing submissions, membership plugins storing access logs, or e-commerce plugins storing order metadata.
  • Popular plugins aren't immune. High-install-count plugins are bigger targets precisely because a single flaw can expose hundreds of thousands of sites at once. Check current install counts and update cadence on WordPress plugin rankings before trusting a dependency.

What the Vulnerable Code Actually Looks Like

Here's the pattern that shows up over and over in disclosed vulnerabilities: a value from the request gets dropped straight into a query string using string concatenation or interpolation.

The vulnerable version

$search = $_GET['s'];
$results = $wpdb->get_results(
  "SELECT * FROM {$wpdb->prefix}my_plugin_entries WHERE title LIKE '%$search%'"
);

An attacker can pass s=%' OR '1'='1 (or worse, a stacked query) and the database executes whatever they wrote, not what the developer intended.

The safe version

$search = sanitize_text_field( $_GET['s'] );
$like = '%' . $wpdb->esc_like( $search ) . '%';
$results = $wpdb->get_results(
  $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}my_plugin_entries WHERE title LIKE %s",
    $like
  )
);

$wpdb->prepare() uses placeholders — %s for strings, %d for integers, %f for floats — and escapes the value before it ever touches the query. esc_like() additionally escapes the wildcard characters % and _ so a search term can't manipulate the LIKE pattern itself.

PatternRiskFix
String concatenation into SQLCritical — direct injection$wpdb->prepare() with placeholders
Table/column names from user inputCritical — placeholders can't quote identifiersWhitelist against a fixed array of allowed names
LIKE queries with raw search termsHigh — wildcard injection, data leakage$wpdb->esc_like() plus %s placeholder
ORDER BY from a query string paramHigh — often forgotten since it's not a "value"Whitelist allowed column names, never pass raw input
$wpdb->insert() / update() without format arraysMedium — type coercion can be exploitedAlways pass the format array, e.g. array('%s','%d')

Where SQL Injection Hides in Real Plugin Features

Some plugin categories carry more risk than others simply because of what they store and query:

  • Form plugins — submissions get written to custom tables, then filtered and exported through admin screens. Contact Form 7 and other entries on our best WordPress form plugins comparison are worth studying for how they sanitize before storage.
  • E-commerce plugins — order search, coupon lookups, and reporting dashboards query by SKU, email, or order ID straight from admin-ajax requests. WooCommerce locks this down with prepared statements throughout its order repository layer.
  • Page builders and widget plugins — dynamic widgets that query posts or custom fields by an ID passed through a shortcode attribute or REST parameter. Elementor is a good reference for how a large, widget-heavy codebase sanitizes dynamic data sources.
  • Caching and performance plugins — anything that purges or queries cache tables by URL or post ID, like LiteSpeed Cache, needs the same discipline even though it feels like "internal" data.

A Practical Checklist Before You Ship

  1. Grep your codebase for $wpdb->query, get_results, get_row, get_var, and get_col — every single one should either contain no user input or run through prepare().
  2. Never build table or column names from user input, even with prepare() — placeholders quote values, not identifiers. Use a hardcoded whitelist array and match against it.
  3. Use $wpdb->insert(), update(), delete(), and replace() instead of hand-written statements wherever possible, and always supply the format array.
  4. Sanitize on input (sanitize_text_field, absint, sanitize_key) and escape again on output — sanitization and SQL escaping solve different problems and neither replaces the other.
  5. Pair every query-handling endpoint with proper nonce verification and capability checks — a SQL-safe query that any logged-out user can trigger is still a serious bug. Our guide to nonces and capability checks covers this in detail.
  6. Run the official Plugin Check plugin (PCP) from WordPress.org before submission — it flags unescaped $wpdb calls automatically, along with several other review-blocking issues.
  7. Add PHPCS with the WordPress security ruleset (WordPress.DB.PreparedSQL, WordPress.Security.EscapeOutput) to CI so the check runs on every commit, not just before release.

What It Costs You If You Get It Wrong

WordPress.org's security team pulls plugins from the directory the moment a confirmed SQL injection report comes in, often with no advance notice to the author. That means:

  • Your plugin drops out of search and the rankings instantly — zero new installs until a patched version is reviewed and reinstated.
  • Existing users get a dashboard warning recommending they deactivate the plugin, which tanks active-install retention even after you fix it.
  • Security-focused aggregators (WPScan, Patchstack) publish the CVE permanently, and it shows up in due-diligence searches for years — including when a buyer is evaluating your plugin via tools like our competitor analysis tool.

If you're choosing between plugins to build on top of, or deciding whether a dependency is safe to recommend to clients, run it through our plugin & theme detector first to see what version a live site is running and cross-check it against known disclosures.

FAQ

What is SQL injection in a WordPress plugin, specifically?

It's when a plugin takes untrusted input — from a URL parameter, form field, or REST request — and inserts it directly into a SQL query string instead of passing it through $wpdb->prepare(). The attacker crafts input that changes the query's logic, letting them read, modify, or delete data they shouldn't have access to.

Does using $wpdb automatically make my plugin safe from SQL injection?

No. $wpdb is just WordPress's database access class — it doesn't escape anything unless you explicitly use prepare(), placeholders, or the built-in insert()/update()/delete() helpers with format arrays. Calling $wpdb->query() with a concatenated string is exactly as vulnerable as raw PHP mysqli code.

Is esc_sql() enough to prevent SQL injection?

esc_sql() escapes special characters but doesn't add quotes around the value or validate its type, so it's easy to misuse. WordPress's own documentation recommends $wpdb->prepare() as the primary method and treats esc_sql() as a fallback for edge cases where placeholders genuinely can't be used.

Can SQL injection happen in the WordPress REST API or AJAX handlers?

Yes, and it's one of the most common places it's found today. Custom REST routes and admin-ajax.php handlers that build queries from $request->get_param() or $_POST without sanitization and preparation are injectable exactly like any other endpoint — REST doesn't add any automatic protection.

How do I check if a plugin I'm using has had a SQL injection vulnerability?

Check the plugin's changelog on WordPress.org for security-related wording, search its slug on WPScan's vulnerability database, and verify the installed version against the patched version. Our plugin & theme detector can confirm exactly which version a live site is running.

Will a SQL injection report get my plugin removed from WordPress.org?

Yes, typically immediately and without warning once the security team verifies the report. The plugin stays closed until you submit a patched version and it passes review, during which time you get zero visibility in search or the directory listings.

Does fixing security issues actually affect plugin rankings and downloads?

Indirectly, yes — a directory removal halts installs completely, and the active-install count (a major ranking signal) stalls or drops while the plugin is closed. Beyond the outage itself, security incidents show up in due-diligence research that marketers and competitors run when comparing plugins, which can cost trust long after the patch ships.

Frequently Asked Questions

It's when a plugin takes untrusted input — from a URL parameter, form field, or REST request — and inserts it directly into a SQL query string instead of passing it through $wpdb->prepare(). The attacker crafts input that changes the query's logic, letting them read, modify, or delete data they shouldn't have access to.
No. $wpdb is just WordPress's database access class — it doesn't escape anything unless you explicitly use prepare(), placeholders, or the built-in insert()/update()/delete() helpers with format arrays. Calling $wpdb->query() with a concatenated string is exactly as vulnerable as raw PHP mysqli code.
esc_sql() escapes special characters but doesn't add quotes around the value or validate its type, so it's easy to misuse. WordPress's own documentation recommends $wpdb->prepare() as the primary method and treats esc_sql() as a fallback for edge cases where placeholders genuinely can't be used.
Yes, and it's one of the most common places it's found today. Custom REST routes and admin-ajax.php handlers that build queries from request parameters or $_POST without sanitization and preparation are injectable exactly like any other endpoint — REST doesn't add any automatic protection.
Check the plugin's changelog on WordPress.org for security-related wording, search its slug on WPScan's vulnerability database, and verify the installed version against the patched version. A plugin and theme detector tool can confirm exactly which version a live site is running.
Yes, typically immediately and without warning once the security team verifies the report. The plugin stays closed until you submit a patched version and it passes review, during which time you get zero visibility in search or the directory listings.
Indirectly, yes — a directory removal halts installs completely, and the active-install count (a major ranking signal) stalls or drops while the plugin is closed. Beyond the outage itself, security incidents show up in due-diligence research that marketers and competitors run when comparing plugins, which can cost trust long after the patch ships.
WP Stats Team

WP Stats Team

The WP Stats team analyzes data from 69,000+ WordPress plugins and 28,000+ themes on WordPress.org. We publish practical, data-backed guidance on plugin SEO, growth, and competitive analysis for developers and marketers.

View all articles by WP Stats Team

Sign In / Register

You need to sign in or register to use this feature.