Blog · Security

SQL Injection Explained with a WordPress Lens

SQL injection is one of the oldest attack techniques on the web, and it still shows up in WordPress vulnerability disclosures every year. Understanding how it actually works, rather than treating it as a vague scary term, is the fastest way to know what to worry about and what to ignore.

What SQL Injection Actually Is

Every WordPress site is backed by a MySQL or MariaDB database. Pages, posts, users, and settings are all rows in tables, and PHP code builds SQL queries to read and write those rows. SQL injection happens when untrusted input, something typed into a search box, a URL parameter, or a form field, gets inserted directly into a SQL query string instead of being treated as data.

Here is the classic vulnerable pattern, simplified:

$id = $_GET['id'];
$result = $wpdb->get_results("SELECT * FROM wp_posts WHERE ID = $id");

If $id is expected to be a number, an attacker can instead send something like 0 OR 1=1 or a payload that closes the query and appends a second one. Because the input is concatenated straight into the SQL string, the database has no way to tell the difference between the intended numeric ID and the attacker’s extra logic. Depending on the query and the database permissions involved, this can leak data, bypass authentication checks, or in worse cases modify or delete data.

Why WordPress Core Is Rarely the Problem

WordPress core has handled this correctly for a very long time. The database abstraction layer, $wpdb, includes a prepare() method built specifically to stop this class of bug:

$id = $_GET['id'];
$result = $wpdb->get_results(
    $wpdb->prepare("SELECT * FROM wp_posts WHERE ID = %d", $id)
);

The %d placeholder tells $wpdb to cast the value to an integer before it ever touches the query string. Text values use %s, which escapes quotes and special characters properly. This is the same idea as parameterized queries or prepared statements in any other language: separate the query structure from the data, and injection stops being possible.

Core, and the vast majority of well-maintained plugins, use this pattern consistently. That is why WordPress itself is very rarely the source of SQL injection vulnerabilities you read about. Serious, well-audited core code paths get scrutinized by a large community, and unsafe query patterns tend to get caught before release.

Where Plugins Introduce the Risk

The overwhelming majority of real-world WordPress SQL injection reports trace back to plugins and, less often, themes. The reason is straightforward: plugins are written by thousands of different developers with wildly different levels of security awareness, and many add custom database queries to support features like custom search, reporting dashboards, import tools, or REST API endpoints.

Common patterns that introduce risk include:

  • Building a query with string concatenation instead of $wpdb->prepare(), especially in admin-side reporting or export features that feel low-risk to the developer.
  • Trusting a REST API parameter, a URL query string, or a cookie value without validating its type or sanitizing it first.
  • Passing user input into an ORDER BY or LIMIT clause, which cannot be parameterized the same way as a value comparison and requires careful whitelisting instead.
  • Older or abandoned plugins that were written before secure coding practices were as widely understood, and that never got updated once the pattern was recognized as risky.

This is also why unmaintained plugins are a real liability even when they still technically function. A plugin that has not been updated in a long time, especially one with a small install base and a lightly reviewed codebase, is a more plausible source of a database-level bug than WordPress core will ever be.

Defense in Depth: Layers That Actually Help

You cannot personally audit every line of every plugin’s SQL queries, and you should not need to. What you can do is stack defenses so that a single mistake in one plugin does not become a full breach.

Keep everything current

Security patches for SQL injection bugs are usually released quietly and quickly once discovered. Staying current closes the window of exposure. From the command line, wp-cli makes this easy to check and act on:

wp core update
wp plugin list --update=available
wp plugin update --all
wp theme update --all

Run these on a schedule, or through your host’s automation, rather than waiting for a manual check.

Least privilege for the database user

The MySQL user WordPress connects with should only have the permissions it actually needs (typically SELECT, INSERT, UPDATE, DELETE on its own database). It should not have DROP, GRANT, or file-level privileges. If a query does get manipulated, a tightly scoped user limits the blast radius considerably.

A web application firewall in front of the site

Cloudflare sitting in front of your origin provides a WAF layer that inspects requests for known injection patterns before they ever reach your server, on top of edge caching, bot mitigation, and DDoS protection. It will not catch every custom or novel payload, but it filters out the large volume of automated, signature-based attack traffic that constantly probes WordPress sites. Keeping your origin IP unpublished behind Cloudflare also matters, since an attacker who cannot reach your server directly cannot bypass the WAF by going around it.

Reduce the attack surface

Every active plugin is additional code that can run a query. Deactivating and removing plugins you are not using, and being deliberate about which ones handle raw database access or custom search functionality, shrinks the number of places a mistake can hide.

Monitor and back up

Logging unusual database errors, watching for spikes in failed queries, and keeping regular offsite backups will not prevent an injection attempt, but they turn a potential disaster into a recoverable incident. A managed host like ServerBorn handles the WAF, server hardening, and backup layer as part of the hosting stack, which takes several of these defenses off your personal to-do list.

Takeaway

SQL injection is a solved problem at the code level: parameterized queries via $wpdb->prepare() stop it cold, and WordPress core follows that discipline consistently. The real risk lives in plugin code that skips this pattern, particularly in less-maintained or custom-built features. Staying current, minimizing database privileges, running a WAF in front of your origin, and trimming unused plugins will not make you immune, but together they make a single coding mistake far less likely to turn into a real breach.