Skip to content

The Basics of $wpdb->prepare() — Why You Should Never Build SQL Queries With String Concatenation

When a WordPress plugin or theme needs to talk to the database directly, it uses the core-provided global object $wpdb. The one thing to absolutely avoid when doing so is dropping user input straight into a SQL string through concatenation. The tool WordPress provides instead is $wpdb->prepare(). This post looks at why that matters and what the function actually does under the hood.

Note: SQL injection is a broad term for attacks where unexpected input gets mixed into a SQL statement an application builds, rewriting what that statement actually tells the database to do. Any value coming from outside the application — form fields, URL parameters, and so on — is the starting point to be suspicious of.

Why String Concatenation Is Dangerous

Consider the following code (purely illustrative pseudo-code, not something to actually run on a live WordPress site):

// Dangerous: user input is dropped straight into the string
$title = $_GET['title'];
$sql = "SELECT * FROM {$wpdb->posts} WHERE post_title = '$title'";
$results = $wpdb->get_results($sql);

This works fine as long as $title is an ordinary string. But if $title is set to something like ' OR '1'='1, the resulting SQL statement becomes:

SELECT * FROM wp_posts WHERE post_title = '' OR '1'='1'

Since '1'='1' is always true, the WHERE clause is effectively neutralized, and a query meant to return one specific post ends up returning every post instead. That’s a simple example, but the same idea can be extended toward retrieving, modifying, or deleting data the application never intended to expose. The root cause is the act of building a SQL statement out of a value that came from outside the application. No amount of careful checking of the value’s contents removes the underlying risk as long as the statement itself is assembled through string concatenation.

What $wpdb->prepare() Actually Solves

The solution WordPress core provides is $wpdb->prepare(), used like this:

// Safe: use a placeholder instead
$title = $_GET['title'];
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title = %s",
    $title
);
$results = $wpdb->get_results($sql);

The %s is a placeholder, and the value passed as the second argument gets inserted there. The key point is that prepare() isn’t doing simple string substitution — it escapes any special characters in the value first, so they can never be interpreted as part of the SQL statement’s structure, before inserting it. Pass the same ' OR '1'='1 value through prepare(), and it’s treated purely as a literal string to search for; the structure of the SQL statement itself never changes.

Why the Placeholder Type Matters

$wpdb->prepare() supports a few main placeholder types.

Placeholder Purpose
%s String
%d Integer
%f Floating-point number

The reason to use %d or %f isn’t only escaping — it’s that the value gets coerced into the expected type. For a post ID field using %d, a value like 123abc would simply become the integer 123. Using %s where a number is expected, on the other hand, wraps the value in quotes as a string literal in the SQL statement, which can lead to unintended behavior or errors. Choosing the placeholder type that matches the meaning of the value being passed is just as important as using prepare() in the first place.

Where Plain Placeholders Fall Short: LIKE and IN

LIKE clauses, commonly used for search features, are a well-known spot where the basic use of prepare() isn’t quite enough on its own. LIKE interprets % and _ as wildcards meaning “any string” and “any single character” respectively, so if a search keyword happens to contain those characters, the query can produce results the user never intended. WordPress core addresses this with a dedicated function, $wpdb->esc_like(), which escapes those wildcard characters before the value is handed to prepare().

$keyword = $wpdb->esc_like($_GET['s']);
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->posts} WHERE post_title LIKE %s",
    '%' . $keyword . '%'
);

Similarly, passing a list of values into an IN (...) clause requires building out a placeholder for each value dynamically — it can’t be handled with a single %s. Being aware that a handful of patterns like these don’t map cleanly onto the basic placeholder syntax is worth keeping in mind whenever $wpdb is involved.

Why This Theme Never Touches $wpdb At All

Looking through the implementation of this blog’s theme, wpmm-blog, $wpdb is never referenced directly anywhere — not in functions.php, not in any template file. Post listings, archives, and search results are all retrieved through WP_Query and template tags (have_posts(), the_post(), and so on), so there’s simply no point in the theme where a raw SQL statement needs to be assembled by hand.

That’s not an accident — it reflects a design principle recommended throughout WordPress theme and plugin development. High-level APIs like WP_Query already perform the equivalent of $wpdb->prepare() internally, so the calling code never has to think about SQL at all. Reaching for the core-provided high-level API instead of writing raw SQL, wherever that’s an option, is just as practical a guideline for avoiding SQL injection as using prepare() correctly when raw SQL genuinely is needed.

Summary

Situation What to do
Inserting an ordinary value into a SQL statement Use $wpdb->prepare() with the right placeholder (%s / %d / %f)
A value used inside a LIKE clause Escape wildcards first with $wpdb->esc_like(), then pass the result to prepare()
A situation where raw SQL can be avoided entirely Let a core API like WP_Query handle it instead of assembling a query by hand

The core of SQL injection defense is turning the mindset of “never trust input” into a concrete habit: using $wpdb->prepare(). It’s an easy problem to miss if string concatenation has become second nature, so whenever you’re reading or writing code that touches $wpdb, it’s worth making a habit of checking first whether placeholders are actually being used.